From 333d28742628ad5ed4edddd29dd728e0e4d3cd3c Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Mon, 6 Apr 2015 15:03:24 -0400 Subject: [PATCH] Integrate wtxmgr package. --- chain/chain.go | 83 ++--- log.go | 10 +- rpcserver.go | 370 ++++++++++---------- wallet/chainntfns.go | 133 ++++---- wallet/config.go | 4 +- wallet/createtx.go | 124 ++++--- wallet/createtx_test.go | 43 ++- wallet/rescan.go | 6 +- wallet/wallet.go | 727 ++++++++++++++++++++++++++-------------- walletsetup.go | 63 ++-- 10 files changed, 894 insertions(+), 669 deletions(-) diff --git a/chain/chain.go b/chain/chain.go index 933de995e0..38c65588d0 100644 --- a/chain/chain.go +++ b/chain/chain.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2014 Conformal Systems LLC + * Copyright (c) 2013-2015 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 @@ -26,8 +26,8 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcrpcclient" "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcwallet/legacy/txstore" "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wtxmgr" ) // Client represents a persistent client connection to a bitcoin RPC server @@ -159,18 +159,11 @@ type ( // BlockStamp was reorganized out of the best chain. BlockDisconnected waddrmgr.BlockStamp - // RecvTx is a notification for a transaction which pays to a wallet - // address. - RecvTx struct { - Tx *btcutil.Tx // Index is guaranteed to be set. - Block *txstore.Block // nil if unmined - } - - // RedeemingTx is a notification for a transaction which spends an - // output controlled by the wallet. - RedeemingTx struct { - Tx *btcutil.Tx // Index is guaranteed to be set. - Block *txstore.Block // nil if unmined + // RelevantTx is a notification for a transaction which spends wallet + // inputs or pays to a watched address. + RelevantTx struct { + TxRecord *wtxmgr.TxRecord + Block *wtxmgr.BlockMeta // nil if unmined } // RescanProgress is a notification describing the current status @@ -209,23 +202,25 @@ func (c *Client) BlockStamp() (*waddrmgr.BlockStamp, error) { } } -// parseBlock parses a btcjson definition of the block a tx is mined it to the -// Block structure of the txstore package, and the block index. This is done +// parseBlock parses a btcws definition of the block a tx is mined it to the +// Block structure of the wtxmgr package, and the block index. This is done // here since btcrpcclient doesn't parse this nicely for us. -func parseBlock(block *btcjson.BlockDetails) (blk *txstore.Block, idx int, err error) { +func parseBlock(block *btcjson.BlockDetails) (*wtxmgr.BlockMeta, error) { if block == nil { - return nil, btcutil.TxIndexUnknown, nil + return nil, nil } blksha, err := wire.NewShaHashFromStr(block.Hash) if err != nil { - return nil, btcutil.TxIndexUnknown, err + return nil, err } - blk = &txstore.Block{ - Height: block.Height, - Hash: *blksha, - Time: time.Unix(block.Time, 0), + blk := &wtxmgr.BlockMeta{ + Block: wtxmgr.Block{ + Height: block.Height, + Hash: *blksha, + }, + Time: time.Unix(block.Time, 0), } - return blk, block.Index, nil + return blk, nil } func (c *Client) onClientConnect() { @@ -242,35 +237,25 @@ func (c *Client) onBlockDisconnected(hash *wire.ShaHash, height int32) { } func (c *Client) onRecvTx(tx *btcutil.Tx, block *btcjson.BlockDetails) { - var blk *txstore.Block - index := btcutil.TxIndexUnknown - if block != nil { - var err error - blk, index, err = parseBlock(block) - if err != nil { - // Log and drop improper notification. - log.Errorf("recvtx notification bad block: %v", err) - return - } + blk, err := parseBlock(block) + if err != nil { + // Log and drop improper notification. + log.Errorf("recvtx notification bad block: %v", err) + return } - tx.SetIndex(index) - c.enqueueNotification <- RecvTx{tx, blk} + + rec, err := wtxmgr.NewTxRecordFromMsgTx(tx.MsgTx(), time.Now()) + if err != nil { + log.Errorf("Cannot create transaction record for relevant "+ + "tx: %v", err) + return + } + c.enqueueNotification <- RelevantTx{rec, blk} } func (c *Client) onRedeemingTx(tx *btcutil.Tx, block *btcjson.BlockDetails) { - var blk *txstore.Block - index := btcutil.TxIndexUnknown - if block != nil { - var err error - blk, index, err = parseBlock(block) - if err != nil { - // Log and drop improper notification. - log.Errorf("redeemingtx notification bad block: %v", err) - return - } - } - tx.SetIndex(index) - c.enqueueNotification <- RedeemingTx{tx, blk} + // Handled exactly like recvtx notifications. + c.onRecvTx(tx, block) } func (c *Client) onRescanProgress(hash *wire.ShaHash, height int32, blkTime time.Time) { diff --git a/log.go b/log.go index 22350770cc..f2a3488a52 100644 --- a/log.go +++ b/log.go @@ -22,7 +22,7 @@ import ( "github.com/btcsuite/btclog" "github.com/btcsuite/btcwallet/chain" - "github.com/btcsuite/btcwallet/legacy/txstore" + "github.com/btcsuite/btcwallet/wtxmgr" "github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/seelog" ) @@ -44,7 +44,7 @@ var ( backendLog = seelog.Disabled log = btclog.Disabled walletLog = btclog.Disabled - txstLog = btclog.Disabled + txmgrLog = btclog.Disabled chainLog = btclog.Disabled ) @@ -52,7 +52,7 @@ var ( var subsystemLoggers = map[string]btclog.Logger{ "BTCW": log, "WLLT": walletLog, - "TXST": txstLog, + "TXST": txmgrLog, "CHNS": chainLog, } @@ -87,8 +87,8 @@ func useLogger(subsystemID string, logger btclog.Logger) { walletLog = logger wallet.UseLogger(logger) case "TXST": - txstLog = logger - txstore.UseLogger(logger) + txmgrLog = logger + wtxmgr.UseLogger(logger) case "CHNS": chainLog = logger chain.UseLogger(logger) diff --git a/rpcserver.go b/rpcserver.go index 3fc4314b74..81ea638bf6 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2014 Conformal Systems LLC + * Copyright (c) 2013-2015 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 @@ -45,9 +45,9 @@ import ( "github.com/btcsuite/btcrpcclient" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcwallet/chain" - "github.com/btcsuite/btcwallet/legacy/txstore" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcwallet/wtxmgr" "github.com/btcsuite/websocket" ) @@ -142,6 +142,24 @@ func checkDefaultAccount(account string) error { return nil } +// confirmed checks whether a transaction at height txHeight has met minconf +// confirmations for a blockchain at height curHeight. +func confirmed(minconf, txHeight, curHeight int32) bool { + return confirms(txHeight, curHeight) >= minconf +} + +// confirms returns the number of confirmations for a transaction in a block at +// height txHeight (or -1 for an unconfirmed tx) given the chain height +// curHeight. +func confirms(txHeight, curHeight int32) int32 { + switch { + case txHeight == -1, txHeight > curHeight: + return 0 + default: + return curHeight - txHeight + 1 + } +} + type websocketClient struct { conn *websocket.Conn authenticated bool @@ -309,10 +327,7 @@ type rpcServer struct { // created. connectedBlocks <-chan waddrmgr.BlockStamp disconnectedBlocks <-chan waddrmgr.BlockStamp - newCredits <-chan txstore.Credit - newDebits <-chan txstore.Debits - minedCredits <-chan txstore.Credit - minedDebits <-chan txstore.Debits + relevantTxs <-chan chain.RelevantTx managerLocked <-chan bool confirmedBalance <-chan btcutil.Amount unconfirmedBalance <-chan btcutil.Amount @@ -1038,8 +1053,7 @@ type ( blockConnected waddrmgr.BlockStamp blockDisconnected waddrmgr.BlockStamp - txCredit txstore.Credit - txDebit txstore.Debits + relevantTx chain.RelevantTx managerLocked bool @@ -1059,36 +1073,30 @@ func (b blockDisconnected) notificationCmds(w *wallet.Wallet) []btcjson.Cmd { return []btcjson.Cmd{n} } -func (c txCredit) notificationCmds(w *wallet.Wallet) []btcjson.Cmd { - blk := w.Manager.SyncedTo() - acctName := waddrmgr.DefaultAccountName - if creditAccount, err := w.CreditAccount(txstore.Credit(c)); err == nil { - // acctName is defaulted to DefaultAccountName in case of an error - acctName, _ = w.Manager.AccountName(creditAccount) +func (t relevantTx) notificationCmds(w *wallet.Wallet) []btcjson.Cmd { + syncBlock := w.Manager.SyncedTo() + + var block *wtxmgr.Block + if t.Block != nil { + block = &t.Block.Block } - ltr, err := txstore.Credit(c).ToJSON(acctName, blk.Height, activeNet.Params) + details, err := w.TxStore.UniqueTxDetails(&t.TxRecord.Hash, block) if err != nil { - log.Errorf("Cannot create notification for transaction "+ - "credit: %v", err) + log.Errorf("Cannot fetch transaction details for "+ + "client notification: %v", err) return nil } - n := btcws.NewTxNtfn(acctName, <r) - return []btcjson.Cmd{n} -} - -func (d txDebit) notificationCmds(w *wallet.Wallet) []btcjson.Cmd { - blk := w.Manager.SyncedTo() - ltrs, err := txstore.Debits(d).ToJSON("", blk.Height, activeNet.Params) - if err != nil { - log.Errorf("Cannot create notification for transaction "+ - "debits: %v", err) + if details == nil { + log.Errorf("No details found for client transaction notification") return nil } - ns := make([]btcjson.Cmd, len(ltrs)) - for i := range ns { - ns[i] = btcws.NewTxNtfn("", <rs[i]) + + ltr := wallet.ListTransactions(details, syncBlock.Height, activeNet.Params) + ntfns := make([]btcjson.Cmd, len(ltr)) + for i := range ntfns { + ntfns[i] = btcws.NewTxNtfn(ltr[i].Account, <r[i]) } - return ns + return ntfns } func (l managerLocked) notificationCmds(w *wallet.Wallet) []btcjson.Cmd { @@ -1121,14 +1129,8 @@ out: s.enqueueNotification <- blockConnected(n) case n := <-s.disconnectedBlocks: s.enqueueNotification <- blockDisconnected(n) - case n := <-s.newCredits: - s.enqueueNotification <- txCredit(n) - case n := <-s.newDebits: - s.enqueueNotification <- txDebit(n) - case n := <-s.minedCredits: - s.enqueueNotification <- txCredit(n) - case n := <-s.minedDebits: - s.enqueueNotification <- txDebit(n) + case n := <-s.relevantTxs: + s.enqueueNotification <- relevantTx(n) case n := <-s.managerLocked: s.enqueueNotification <- managerLocked(n) case n := <-s.confirmedBalance: @@ -1153,28 +1155,10 @@ out: err) continue } - newCredits, err := s.wallet.TxStore.ListenNewCredits() + relevantTxs, err := s.wallet.ListenRelevantTxs() if err != nil { - log.Errorf("Could not register for new "+ - "credit notifications: %v", err) - continue - } - newDebits, err := s.wallet.TxStore.ListenNewDebits() - if err != nil { - log.Errorf("Could not register for new "+ - "debit notifications: %v", err) - continue - } - minedCredits, err := s.wallet.TxStore.ListenMinedCredits() - if err != nil { - log.Errorf("Could not register for mined "+ - "credit notifications: %v", err) - continue - } - minedDebits, err := s.wallet.TxStore.ListenMinedDebits() - if err != nil { - log.Errorf("Could not register for mined "+ - "debit notifications: %v", err) + log.Errorf("Could not register for new relevant "+ + "transaction notifications: %v", err) continue } managerLocked, err := s.wallet.ListenLockStatus() @@ -1197,10 +1181,7 @@ out: } s.connectedBlocks = connectedBlocks s.disconnectedBlocks = disconnectedBlocks - s.newCredits = newCredits - s.newDebits = newDebits - s.minedCredits = minedCredits - s.minedDebits = minedDebits + s.relevantTxs = relevantTxs s.managerLocked = managerLocked s.confirmedBalance = confirmedBalance s.unconfirmedBalance = unconfirmedBalance @@ -1219,10 +1200,8 @@ func (s *rpcServer) drainNotifications() { select { case <-s.connectedBlocks: case <-s.disconnectedBlocks: - case <-s.newCredits: - case <-s.newDebits: - case <-s.minedCredits: - case <-s.minedDebits: + case <-s.relevantTxs: + case <-s.managerLocked: case <-s.confirmedBalance: case <-s.unconfirmedBalance: case <-s.registerWalletNtfns: @@ -1694,13 +1673,13 @@ func GetBalance(w *wallet.Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (int var account uint32 var err error if cmd.Account == nil || *cmd.Account == "*" { - balance, err = w.CalculateBalance(cmd.MinConf) + balance, err = w.CalculateBalance(int32(cmd.MinConf)) } else { account, err = w.Manager.LookupAccount(*cmd.Account) if err != nil { return nil, err } - balance, err = w.CalculateAccountBalance(account, cmd.MinConf) + balance, err = w.CalculateAccountBalance(account, int32(cmd.MinConf)) } if err != nil { return nil, err @@ -1960,7 +1939,7 @@ func GetReceivedByAccount(w *wallet.Wallet, chainSvr *chain.Client, icmd btcjson return nil, err } - bal, _, err := w.TotalReceivedForAccount(account, cmd.MinConf) + bal, _, err := w.TotalReceivedForAccount(account, int32(cmd.MinConf)) if err != nil { return nil, err } @@ -1977,7 +1956,7 @@ func GetReceivedByAddress(w *wallet.Wallet, chainSvr *chain.Client, icmd btcjson if err != nil { return nil, InvalidAddressOrKeyError{err} } - total, err := w.TotalReceivedForAddr(addr, cmd.MinConf) + total, err := w.TotalReceivedForAddr(addr, int32(cmd.MinConf)) if err != nil { return nil, err } @@ -1995,96 +1974,105 @@ func GetTransaction(w *wallet.Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) return nil, btcjson.ErrDecodeHexString } - record, ok := w.TxRecord(txSha) - if !ok { + details, err := w.TxStore.TxDetails(txSha) + if err != nil { + return nil, err + } + if details == nil { return nil, btcjson.ErrNoTxInfo } - blk := w.Manager.SyncedTo() + syncBlock := w.Manager.SyncedTo() + // TODO: The serialized transaction is already in the DB, so + // reserializing can be avoided here. var txBuf bytes.Buffer - txBuf.Grow(record.Tx().MsgTx().SerializeSize()) - err = record.Tx().MsgTx().Serialize(&txBuf) + txBuf.Grow(details.MsgTx.SerializeSize()) + err = details.MsgTx.Serialize(&txBuf) if err != nil { return nil, err } - // TODO(jrick) set "generate" to true if this is the coinbase (if - // record.Tx().Index() == 0). + // TODO: Add a "generated" field to this result type. "generated":true + // is only added if the transaction is a coinbase. ret := btcjson.GetTransactionResult{ - TxID: txSha.String(), + TxID: cmd.Txid, Hex: hex.EncodeToString(txBuf.Bytes()), - Time: record.Received().Unix(), - TimeReceived: record.Received().Unix(), - WalletConflicts: []string{}, + Time: details.Received.Unix(), + TimeReceived: details.Received.Unix(), + WalletConflicts: []string{}, // Not saved + //Generated: blockchain.IsCoinBaseTx(&details.MsgTx), } - if record.BlockHeight != -1 { - txBlock, err := record.Block() - if err != nil { - return nil, err + if details.Block.Height != -1 { + ret.BlockHash = details.Block.Hash.String() + ret.BlockTime = details.Block.Time.Unix() + ret.Confirmations = int64(confirms(details.Block.Height, syncBlock.Height)) + } + + var ( + debitTotal btcutil.Amount + creditTotal btcutil.Amount // Excludes change + outputTotal btcutil.Amount + fee btcutil.Amount + feeF64 float64 + ) + for _, deb := range details.Debits { + debitTotal += deb.Amount + } + for _, cred := range details.Credits { + if !cred.Change { + creditTotal += cred.Amount } - ret.BlockIndex = int64(record.Tx().Index()) - ret.BlockHash = txBlock.Hash.String() - ret.BlockTime = txBlock.Time.Unix() - ret.Confirmations = int64(record.Confirmations(blk.Height)) + } + for _, output := range details.MsgTx.TxOut { + outputTotal -= btcutil.Amount(output.Value) + } + // Fee can only be determined if every input is a debit. + if len(details.Debits) == len(details.MsgTx.TxIn) { + fee = debitTotal - outputTotal + feeF64 = fee.ToBTC() } - credits := record.Credits() - debits, err := record.Debits() - var targetAddr *string - var creditAmount btcutil.Amount - if err != nil { + if len(details.Debits) == 0 { // Credits must be set later, but since we know the full length // of the details slice, allocate it with the correct cap. - ret.Details = make([]btcjson.GetTransactionDetailsResult, 0, len(credits)) + ret.Details = make([]btcjson.GetTransactionDetailsResult, 0, len(details.Credits)) } else { - ret.Details = make([]btcjson.GetTransactionDetailsResult, 1, len(credits)+1) + ret.Details = make([]btcjson.GetTransactionDetailsResult, 1, len(details.Credits)+1) - details := btcjson.GetTransactionDetailsResult{ + ret.Details[0] = btcjson.GetTransactionDetailsResult{ Account: waddrmgr.DefaultAccountName, Category: "send", - // negative since it is a send - Amount: (-debits.OutputAmount(true)).ToBTC(), - Fee: debits.Fee().ToBTC(), + Amount: (-debitTotal).ToBTC(), // negative since it is a send + Fee: feeF64, } - targetAddr = &details.Address - ret.Details[0] = details - ret.Fee = details.Fee - - creditAmount = -debits.InputAmount() + ret.Fee = feeF64 } - for _, cred := range record.Credits() { + credCat := wallet.RecvCategory(details, syncBlock.Height).String() + for _, cred := range details.Credits { // Change is ignored. - if cred.Change() { + if cred.Change { continue } - creditAmount += cred.Amount() - var addr string - // Errors don't matter here, as we only consider the - // case where len(addrs) == 1. - _, addrs, _, _ := cred.Addresses(activeNet.Params) - if len(addrs) == 1 { + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + details.MsgTx.TxOut[cred.Index].PkScript, activeNet.Params) + if err == nil && len(addrs) == 1 { addr = addrs[0].EncodeAddress() - // The first non-change output address is considered the - // target for sent transactions. - if targetAddr != nil && *targetAddr == "" { - *targetAddr = addr - } } ret.Details = append(ret.Details, btcjson.GetTransactionDetailsResult{ Account: waddrmgr.DefaultAccountName, - Category: cred.Category(blk.Height).String(), - Amount: cred.Amount().ToBTC(), + Category: credCat, + Amount: cred.Amount.ToBTC(), Address: addr, }) } - ret.Amount = creditAmount.ToBTC() + ret.Amount = creditTotal.ToBTC() return ret, nil } @@ -2103,7 +2091,7 @@ func ListAccounts(w *wallet.Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (i if err != nil { return nil, ErrAccountNameNotFound } - bal, err := w.CalculateAccountBalance(account, cmd.MinConf) + bal, err := w.CalculateAccountBalance(account, int32(cmd.MinConf)) if err != nil { return nil, err } @@ -2143,7 +2131,8 @@ func ListReceivedByAccount(w *wallet.Wallet, chainSvr *chain.Client, icmd btcjso if err != nil { return nil, ErrAccountNameNotFound } - bal, confirmations, err := w.TotalReceivedForAccount(account, cmd.MinConf) + bal, confirmations, err := w.TotalReceivedForAccount(account, + int32(cmd.MinConf)) if err != nil { return nil, err } @@ -2182,7 +2171,7 @@ func ListReceivedByAddress(w *wallet.Wallet, chainSvr *chain.Client, icmd btcjso account string } - blk := w.Manager.SyncedTo() + syncBlock := w.Manager.SyncedTo() // Intermediate data for all addresses. allAddrData := make(map[string]AddrData) @@ -2198,35 +2187,46 @@ func ListReceivedByAddress(w *wallet.Wallet, chainSvr *chain.Client, icmd btcjso allAddrData[address] = AddrData{} } } - for _, record := range w.TxStore.Records() { - for _, credit := range record.Credits() { - confirmations := credit.Confirmations(blk.Height) - if !credit.Confirmed(cmd.MinConf, blk.Height) { - // Not enough confirmations, skip the current block. - continue - } - _, addresses, _, err := credit.Addresses(activeNet.Params) - if err != nil { - // Unusable address, skip it. - continue - } - for _, address := range addresses { - addrStr := address.EncodeAddress() - addrData, ok := allAddrData[addrStr] - if ok { - addrData.amount += credit.Amount() - // Always overwrite confirmations with newer ones. - addrData.confirmations = confirmations - } else { - addrData = AddrData{ - amount: credit.Amount(), - confirmations: confirmations, + + var endHeight int32 + if cmd.MinConf == -1 { + endHeight = -1 + } else { + endHeight = syncBlock.Height - int32(cmd.MinConf) + 1 + } + err := w.TxStore.RangeTransactions(0, endHeight, func(details []wtxmgr.TxDetails) (bool, error) { + confirmations := confirms(details[0].Block.Height, syncBlock.Height) + for _, tx := range details { + for _, cred := range tx.Credits { + pkScript := tx.MsgTx.TxOut[cred.Index].PkScript + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + pkScript, activeNet.Params) + if err != nil { + // Non standard script, skip. + continue + } + for _, addr := range addrs { + addrStr := addr.EncodeAddress() + addrData, ok := allAddrData[addrStr] + if ok { + addrData.amount += cred.Amount + // Always overwrite confirmations with newer ones. + addrData.confirmations = confirmations + } else { + addrData = AddrData{ + amount: cred.Amount, + confirmations: confirmations, + } } + addrData.tx = append(addrData.tx, tx.Hash.String()) + allAddrData[addrStr] = addrData } - addrData.tx = append(addrData.tx, credit.Tx().Sha().String()) - allAddrData[addrStr] = addrData } } + return false, nil + }) + if err != nil { + return nil, err } // Massage address data into output format. @@ -2251,7 +2251,14 @@ func ListReceivedByAddress(w *wallet.Wallet, chainSvr *chain.Client, icmd btcjso func ListSinceBlock(w *wallet.Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { cmd := icmd.(*btcjson.ListSinceBlockCmd) - height := int32(-1) + syncBlock := w.Manager.SyncedTo() + + // For the result we need the block hash for the last block counted + // in the blockchain due to confirmations. We send this off now so that + // it can arrive asynchronously while we figure out the rest. + gbh := chainSvr.GetBlockHashAsync(int64(syncBlock.Height) + 1 - int64(cmd.TargetConfirmations)) + + var start int32 if cmd.BlockHash != "" { hash, err := wire.NewShaHashFromStr(cmd.BlockHash) if err != nil { @@ -2261,18 +2268,11 @@ func ListSinceBlock(w *wallet.Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) if err != nil { return nil, err } - height = int32(block.Height()) + start = int32(block.Height()) + 1 } + end := syncBlock.Height - int32(cmd.TargetConfirmations) + 1 - blk := w.Manager.SyncedTo() - - // For the result we need the block hash for the last block counted - // in the blockchain due to confirmations. We send this off now so that - // it can arrive asynchronously while we figure out the rest. - gbh := chainSvr.GetBlockHashAsync(int64(blk.Height) + 1 - int64(cmd.TargetConfirmations)) - - txInfoList, err := w.ListSinceBlock(height, blk.Height, - cmd.TargetConfirmations) + txInfoList, err := w.ListSinceBlock(start, end, syncBlock.Height) if err != nil { return nil, err } @@ -2373,7 +2373,7 @@ func ListUnspent(w *wallet.Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (in } } - return w.ListUnspent(cmd.MinConf, cmd.MaxConf, addresses) + return w.ListUnspent(int32(cmd.MinConf), int32(cmd.MaxConf), addresses) } // LockUnspent handles the lockunspent command. @@ -2401,9 +2401,10 @@ func LockUnspent(w *wallet.Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (in } // sendPairs is a helper routine to reduce duplicated code when creating and -// sending payment transactions. +// sending payment transactions. It returns the transaction hash in string +// format upon success. func sendPairs(w *wallet.Wallet, chainSvr *chain.Client, cmd btcjson.Cmd, - amounts map[string]btcutil.Amount, account uint32, minconf int) (interface{}, error) { + amounts map[string]btcutil.Amount, account uint32, minconf int32) (string, error) { // Create transaction, replying with an error if the creation // was not successful. @@ -2411,41 +2412,44 @@ func sendPairs(w *wallet.Wallet, chainSvr *chain.Client, cmd btcjson.Cmd, if err != nil { switch { case err == wallet.ErrNonPositiveAmount: - return nil, ErrNeedPositiveAmount + return "", ErrNeedPositiveAmount case isManagerLockedError(err): - return nil, btcjson.ErrWalletUnlockNeeded + return "", btcjson.ErrWalletUnlockNeeded } - return nil, err + return "", err } - // Add to transaction store. - txr, err := w.TxStore.InsertTx(createdTx.Tx, nil) + // Create transaction record and insert into the db. + rec, err := wtxmgr.NewTxRecordFromMsgTx(createdTx.MsgTx, time.Now()) if err != nil { - log.Errorf("Error adding sent tx history: %v", err) - return nil, btcjson.ErrInternal + log.Errorf("Cannot create record for created transaction: %v", err) + return "", btcjson.ErrInternal } - _, err = txr.AddDebits() + err = w.TxStore.InsertTx(rec, nil) if err != nil { log.Errorf("Error adding sent tx history: %v", err) - return nil, btcjson.ErrInternal + return "", btcjson.ErrInternal } + if createdTx.ChangeIndex >= 0 { - _, err = txr.AddCredit(uint32(createdTx.ChangeIndex), true) + err = w.TxStore.AddCredit(rec, nil, uint32(createdTx.ChangeIndex), true) if err != nil { log.Errorf("Error adding change address for sent "+ "tx: %v", err) - return nil, btcjson.ErrInternal + return "", btcjson.ErrInternal } } - w.TxStore.MarkDirty() - txSha, err := chainSvr.SendRawTransaction(createdTx.Tx.MsgTx(), false) + // TODO: The record already has the serialized tx, so no need to + // serialize it again. + txSha, err := chainSvr.SendRawTransaction(&rec.MsgTx, false) if err != nil { - return nil, err + return "", err } - log.Infof("Successfully sent transaction %v", txSha) - return txSha.String(), nil + txShaStr := txSha.String() + log.Infof("Successfully sent transaction %v", txShaStr) + return txShaStr, nil } // SendFrom handles a sendfrom RPC request by creating a new transaction @@ -2473,7 +2477,7 @@ func SendFrom(w *wallet.Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (inter cmd.ToAddress: btcutil.Amount(cmd.Amount), } - return sendPairs(w, chainSvr, cmd, pairs, account, cmd.MinConf) + return sendPairs(w, chainSvr, cmd, pairs, account, int32(cmd.MinConf)) } // SendMany handles a sendmany RPC request by creating a new transaction @@ -2500,7 +2504,7 @@ func SendMany(w *wallet.Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (inter pairs[k] = btcutil.Amount(v) } - return sendPairs(w, chainSvr, cmd, pairs, account, cmd.MinConf) + return sendPairs(w, chainSvr, cmd, pairs, account, int32(cmd.MinConf)) } // SendToAddress handles a sendtoaddress RPC request by creating a new diff --git a/wallet/chainntfns.go b/wallet/chainntfns.go index 3e01cfbde5..a6c4fb8cf7 100644 --- a/wallet/chainntfns.go +++ b/wallet/chainntfns.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2014 Conformal Systems LLC + * Copyright (c) 2013-2015 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,10 +18,9 @@ package wallet import ( "github.com/btcsuite/btcd/txscript" - "github.com/btcsuite/btcutil" "github.com/btcsuite/btcwallet/chain" - "github.com/btcsuite/btcwallet/legacy/txstore" "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wtxmgr" ) func (w *Wallet) handleChainNotifications() { @@ -45,10 +44,8 @@ func (w *Wallet) handleChainNotifications() { w.connectBlock(waddrmgr.BlockStamp(n)) case chain.BlockDisconnected: w.disconnectBlock(waddrmgr.BlockStamp(n)) - case chain.RecvTx: - err = w.addReceivedTx(n.Tx, n.Block) - case chain.RedeemingTx: - err = w.addRedeemingTx(n.Tx, n.Block) + case chain.RelevantTx: + err = w.addRelevantTx(n.TxRecord, n.Block) // The following are handled by the wallet's rescan // goroutines, so just pass them there. @@ -110,69 +107,91 @@ func (w *Wallet) disconnectBlock(bs waddrmgr.BlockStamp) { w.notifyBalances(bs.Height - 1) } -func (w *Wallet) addReceivedTx(tx *btcutil.Tx, block *txstore.Block) error { - // For every output, if it pays to a wallet address, insert the - // transaction into the store (possibly moving it from unconfirmed to - // confirmed), and add a credit record if one does not already exist. - var txr *txstore.TxRecord - txInserted := false - for txOutIdx, txOut := range tx.MsgTx().TxOut { - // Errors don't matter here. If addrs is nil, the range below - // does nothing. - _, addrs, _, _ := txscript.ExtractPkScriptAddrs(txOut.PkScript, +func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, block *wtxmgr.BlockMeta) error { + // TODO: The transaction store and address manager need to be updated + // together, but each operate under different namespaces and are changed + // under new transactions. This is not error safe as we lose + // transaction semantics. + // + // I'm unsure of the best way to solve this. Some possible solutions + // and drawbacks: + // + // 1. Open write transactions here and pass the handle to every + // waddrmr and wtxmgr method. This complicates the caller code + // everywhere, however. + // + // 2. Move the wtxmgr namespace into the waddrmgr namespace, likely + // under its own bucket. This entire function can then be moved + // into the waddrmgr package, which updates the nested wtxmgr. + // This removes some of separation between the components. + // + // 3. Use multiple wtxmgrs, one for each account, nested in the + // waddrmgr namespace. This still provides some sort of logical + // separation (transaction handling remains in another package, and + // is simply used by waddrmgr), but may result in duplicate + // transactions being saved if they are relevant to multiple + // accounts. + // + // 4. Store wtxmgr-related details under the waddrmgr namespace, but + // solve the drawback of #3 by splitting wtxmgr to save entire + // transaction records globally for all accounts, with + // credit/debit/balance tracking per account. Each account would + // also save the relevant transaction hashes and block incidence so + // the full transaction can be loaded from the waddrmgr + // transactions bucket. This currently seems like the best + // solution. + + // At the moment all notified transactions are assumed to actually be + // relevant. This assumption will not hold true when SPV support is + // added, but until then, simply insert the transaction because there + // should either be one or more relevant inputs or outputs. + err := w.TxStore.InsertTx(rec, block) + if err != nil { + return err + } + + // Check every output to determine whether it is controlled by a wallet + // key. If so, mark the output as a credit. + for i, output := range rec.MsgTx.TxOut { + _, addrs, _, err := txscript.ExtractPkScriptAddrs(output.PkScript, w.chainParams) - insert := false + if err != nil { + // Non-standard outputs are skipped. + continue + } for _, addr := range addrs { - _, err := w.Manager.Address(addr) + ma, err := w.Manager.Address(addr) if err == nil { - insert = true - break - } - } - if insert { - if !txInserted { - var err error - txr, err = w.TxStore.InsertTx(tx, block) + // TODO: Credits should be added with the + // account they belong to, so wtxmgr is able to + // track per-account balances. + err = w.TxStore.AddCredit(rec, block, uint32(i), + ma.Internal()) if err != nil { return err } - // InsertTx may have moved a previous unmined - // tx, so mark the entire store as dirty. - w.TxStore.MarkDirty() - txInserted = true - } - if txr.HasCredit(txOutIdx) { + // TODO: What is an address id? Code copied + // from master but no idea if correct. Needs a + // fix. + addressID := addr.ScriptAddress() + err = w.Manager.MarkUsed(addressID) + if err != nil { + return err + } + log.Debugf("Marked address %v used", addr) continue } - _, err := txr.AddCredit(uint32(txOutIdx), false) - if err != nil { + + // Missing addresses are skipped. Other errors should + // be propigated. + code := err.(waddrmgr.ManagerError).ErrorCode + if code != waddrmgr.ErrAddressNotFound { return err } - w.TxStore.MarkDirty() } } - bs, err := w.chainSvr.BlockStamp() - if err == nil { - w.notifyBalances(bs.Height) - } - - return nil -} - -// addRedeemingTx inserts the notified spending transaction as a debit and -// schedules the transaction store for a future file write. -func (w *Wallet) addRedeemingTx(tx *btcutil.Tx, block *txstore.Block) error { - txr, err := w.TxStore.InsertTx(tx, block) - if err != nil { - return err - } - if _, err := txr.AddDebits(); err != nil { - return err - } - if err := w.markAddrsUsed(txr); err != nil { - return err - } + // TODO: Notify connected clients of the added transaction. bs, err := w.chainSvr.BlockStamp() if err == nil { diff --git a/wallet/config.go b/wallet/config.go index 16ee823c6b..95e4070cad 100644 --- a/wallet/config.go +++ b/wallet/config.go @@ -18,9 +18,9 @@ package wallet import ( "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcwallet/legacy/txstore" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/walletdb" + "github.com/btcsuite/btcwallet/wtxmgr" ) // Config is a structure used to initialize a Wallet @@ -28,6 +28,6 @@ import ( type Config struct { ChainParams *chaincfg.Params Db *walletdb.DB - TxStore *txstore.Store + TxStore *wtxmgr.Store Waddrmgr *waddrmgr.Manager } diff --git a/wallet/createtx.go b/wallet/createtx.go index a638ad91da..318854939a 100644 --- a/wallet/createtx.go +++ b/wallet/createtx.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2014 Conformal Systems LLC + * Copyright (c) 2013-2015 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,8 +28,8 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcwallet/legacy/txstore" "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wtxmgr" ) const ( @@ -109,17 +109,17 @@ const defaultFeeIncrement = 1e3 // CreatedTx holds the state of a newly-created transaction and the change // output (if one was added). type CreatedTx struct { - Tx *btcutil.Tx + MsgTx *wire.MsgTx ChangeAddr btcutil.Address ChangeIndex int // negative if no change } // ByAmount defines the methods needed to satisify sort.Interface to // sort a slice of Utxos by their amount. -type ByAmount []txstore.Credit +type ByAmount []wtxmgr.Credit func (u ByAmount) Len() int { return len(u) } -func (u ByAmount) Less(i, j int) bool { return u[i].Amount() < u[j].Amount() } +func (u ByAmount) Less(i, j int) bool { return u[i].Amount < u[j].Amount } func (u ByAmount) Swap(i, j int) { u[i], u[j] = u[j], u[i] } // txToPairs creates a raw transaction sending the amounts for each @@ -129,7 +129,7 @@ func (u ByAmount) Swap(i, j int) { u[i], u[j] = u[j], u[i] } // to addr or as a fee for the miner are sent to a newly generated // address. InsufficientFundsError is returned if there are not enough // eligible unspent outputs to create the transaction. -func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, account uint32, minconf int) (*CreatedTx, error) { +func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, account uint32, minconf int32) (*CreatedTx, error) { // Address manager must be unlocked to compose transaction. Grab // the unlock if possible (to prevent future unlocks), or return the @@ -159,7 +159,7 @@ func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, account uint32, minc // the mining fee. It then creates and returns a CreatedTx containing // the selected inputs and the given outputs, validating it (using // validateMsgTx) as well. -func createTx(eligible []txstore.Credit, +func createTx(eligible []wtxmgr.Credit, outputs map[string]btcutil.Amount, bs *waddrmgr.BlockStamp, feeIncrement btcutil.Amount, mgr *waddrmgr.Manager, account uint32, changeAddress func(account uint32) (btcutil.Address, error), @@ -177,8 +177,8 @@ func createTx(eligible []txstore.Credit, // Start by adding enough inputs to cover for the total amount of all // desired outputs. - var input txstore.Credit - var inputs []txstore.Credit + var input wtxmgr.Credit + var inputs []wtxmgr.Credit totalAdded := btcutil.Amount(0) for totalAdded < minAmount { if len(eligible) == 0 { @@ -186,8 +186,8 @@ func createTx(eligible []txstore.Credit, } input, eligible = eligible[0], eligible[1:] inputs = append(inputs, input) - msgtx.AddTxIn(wire.NewTxIn(input.OutPoint(), nil)) - totalAdded += input.Amount() + msgtx.AddTxIn(wire.NewTxIn(&input.OutPoint, nil)) + totalAdded += input.Amount } // Get an initial fee estimate based on the number of selected inputs @@ -204,9 +204,9 @@ func createTx(eligible []txstore.Credit, } input, eligible = eligible[0], eligible[1:] inputs = append(inputs, input) - msgtx.AddTxIn(wire.NewTxIn(input.OutPoint(), nil)) + msgtx.AddTxIn(wire.NewTxIn(&input.OutPoint, nil)) szEst += txInEstimate - totalAdded += input.Amount() + totalAdded += input.Amount feeEst = minimumFee(feeIncrement, szEst, msgtx.TxOut, inputs, bs.Height, disallowFree) } @@ -255,9 +255,9 @@ func createTx(eligible []txstore.Credit, } input, eligible = eligible[0], eligible[1:] inputs = append(inputs, input) - msgtx.AddTxIn(wire.NewTxIn(input.OutPoint(), nil)) + msgtx.AddTxIn(wire.NewTxIn(&input.OutPoint, nil)) szEst += txInEstimate - totalAdded += input.Amount() + totalAdded += input.Amount feeEst = minimumFee(feeIncrement, szEst, msgtx.TxOut, inputs, bs.Height, disallowFree) } } @@ -267,7 +267,7 @@ func createTx(eligible []txstore.Credit, } info := &CreatedTx{ - Tx: btcutil.NewTx(msgtx), + MsgTx: msgtx, ChangeAddr: changeAddr, ChangeIndex: changeIdx, } @@ -316,44 +316,59 @@ func addOutputs(msgtx *wire.MsgTx, pairs map[string]btcutil.Amount, chainParams return minAmount, nil } -func (w *Wallet) findEligibleOutputs(account uint32, minconf int, bs *waddrmgr.BlockStamp) ([]txstore.Credit, error) { +func (w *Wallet) findEligibleOutputs(account uint32, minconf int32, bs *waddrmgr.BlockStamp) ([]wtxmgr.Credit, error) { unspent, err := w.TxStore.UnspentOutputs() if err != nil { return nil, err } - // Filter out unspendable outputs, that is, remove those that (at this - // time) are not P2PKH outputs. Other inputs must be manually included - // in transactions and sent (for example, using createrawtransaction, - // signrawtransaction, and sendrawtransaction). - eligible := make([]txstore.Credit, 0, len(unspent)) + + // TODO: Eventually all of these filters (except perhaps output locking) + // should be handled by the call to UnspentOutputs (or similar). + // Because one of these filters requires matching the output script to + // the desired account, this change depends on making wtxmgr a waddrmgr + // dependancy and requesting unspent outputs for a single account. + eligible := make([]wtxmgr.Credit, 0, len(unspent)) for i := range unspent { - switch txscript.GetScriptClass(unspent[i].TxOut().PkScript) { - case txscript.PubKeyHashTy: - if !unspent[i].Confirmed(minconf, bs.Height) { - continue - } - // Coinbase transactions must have have reached maturity - // before their outputs may be spent. - if unspent[i].IsCoinbase() { - target := blockchain.CoinbaseMaturity - if !unspent[i].Confirmed(target, bs.Height) { - continue - } - } + output := &unspent[i] - // Locked unspent outputs are skipped. - if w.LockedOutpoint(*unspent[i].OutPoint()) { + // Only include this output if it meets the required number of + // confirmations. Coinbase transactions must have have reached + // maturity before their outputs may be spent. + if !confirmed(minconf, output.Height, bs.Height) { + continue + } + if output.FromCoinBase { + const target = blockchain.CoinbaseMaturity + if !confirmed(target, output.Height, bs.Height) { continue } + } - creditAccount, err := w.CreditAccount(unspent[i]) - if err != nil { - continue - } - if creditAccount == account { - eligible = append(eligible, unspent[i]) - } + // Locked unspent outputs are skipped. + if w.LockedOutpoint(output.OutPoint) { + continue + } + + // Filter out unspendable outputs, that is, remove those that + // (at this time) are not P2PKH outputs. Other inputs must be + // manually included in transactions and sent (for example, + // using createrawtransaction, signrawtransaction, and + // sendrawtransaction). + class, addrs, _, err := txscript.ExtractPkScriptAddrs( + output.PkScript, w.chainParams) + if err != nil || class != txscript.PubKeyHashTy { + continue } + + // Only include the output if it is associated with the passed + // account. There should only be one address since this is a + // P2PKH script. + addrAcct, err := w.Manager.AddrAccount(addrs[0]) + if err != nil || addrAcct != account { + continue + } + + eligible = append(eligible, *output) } return eligible, nil } @@ -361,7 +376,7 @@ func (w *Wallet) findEligibleOutputs(account uint32, minconf int, bs *waddrmgr.B // signMsgTx sets the SignatureScript for every item in msgtx.TxIn. // It must be called every time a msgtx is changed. // Only P2PKH outputs are supported at this point. -func signMsgTx(msgtx *wire.MsgTx, prevOutputs []txstore.Credit, mgr *waddrmgr.Manager, chainParams *chaincfg.Params) error { +func signMsgTx(msgtx *wire.MsgTx, prevOutputs []wtxmgr.Credit, mgr *waddrmgr.Manager, chainParams *chaincfg.Params) error { if len(prevOutputs) != len(msgtx.TxIn) { return fmt.Errorf( "Number of prevOutputs (%d) does not match number of tx inputs (%d)", @@ -370,7 +385,8 @@ func signMsgTx(msgtx *wire.MsgTx, prevOutputs []txstore.Credit, mgr *waddrmgr.Ma for i, output := range prevOutputs { // Errors don't matter here, as we only consider the // case where len(addrs) == 1. - _, addrs, _, _ := output.Addresses(chainParams) + _, addrs, _, _ := txscript.ExtractPkScriptAddrs(output.PkScript, + chainParams) if len(addrs) != 1 { continue } @@ -391,7 +407,7 @@ func signMsgTx(msgtx *wire.MsgTx, prevOutputs []txstore.Credit, mgr *waddrmgr.Ma } sigscript, err := txscript.SignatureScript(msgtx, i, - output.TxOut().PkScript, txscript.SigHashAll, privkey, + output.PkScript, txscript.SigHashAll, privkey, ai.Compressed()) if err != nil { return fmt.Errorf("cannot create sigscript: %s", err) @@ -402,11 +418,11 @@ func signMsgTx(msgtx *wire.MsgTx, prevOutputs []txstore.Credit, mgr *waddrmgr.Ma return nil } -func validateMsgTx(msgtx *wire.MsgTx, prevOutputs []txstore.Credit) error { +func validateMsgTx(msgtx *wire.MsgTx, prevOutputs []wtxmgr.Credit) error { for i, txin := range msgtx.TxIn { engine, err := txscript.NewScript( - txin.SignatureScript, prevOutputs[i].TxOut().PkScript, - i, msgtx, txscript.StandardVerifyFlags) + txin.SignatureScript, prevOutputs[i].PkScript, i, msgtx, + txscript.StandardVerifyFlags) if err != nil { return fmt.Errorf("cannot create script engine: %s", err) } @@ -422,7 +438,7 @@ func validateMsgTx(msgtx *wire.MsgTx, prevOutputs []txstore.Credit) error { // s less than 1 kilobyte and none of the outputs contain a value // less than 1 bitcent. Otherwise, the fee will be calculated using // incr, incrementing the fee for each kilobyte of transaction. -func minimumFee(incr btcutil.Amount, txLen int, outputs []*wire.TxOut, prevOutputs []txstore.Credit, height int32, disallowFree bool) btcutil.Amount { +func minimumFee(incr btcutil.Amount, txLen int, outputs []*wire.TxOut, prevOutputs []wtxmgr.Credit, height int32, disallowFree bool) btcutil.Amount { allowFree := false if !disallowFree { allowFree = allowNoFeeTx(height, prevOutputs, txLen) @@ -452,15 +468,15 @@ func minimumFee(incr btcutil.Amount, txLen int, outputs []*wire.TxOut, prevOutpu // allowNoFeeTx calculates the transaction priority and checks that the // priority reaches a certain threshold. If the threshhold is // reached, a free transaction fee is allowed. -func allowNoFeeTx(curHeight int32, txouts []txstore.Credit, txSize int) bool { +func allowNoFeeTx(curHeight int32, txouts []wtxmgr.Credit, txSize int) bool { const blocksPerDayEstimate = 144.0 const txSizeEstimate = 250.0 const threshold = btcutil.SatoshiPerBitcoin * blocksPerDayEstimate / txSizeEstimate var weightedSum int64 for _, txout := range txouts { - depth := chainDepth(txout.BlockHeight, curHeight) - weightedSum += int64(txout.Amount()) * int64(depth) + depth := chainDepth(txout.Height, curHeight) + weightedSum += int64(txout.Amount) * int64(depth) } priority := float64(weightedSum) / float64(txSize) return priority > threshold diff --git a/wallet/createtx_test.go b/wallet/createtx_test.go index 347fafb999..7650e9b603 100644 --- a/wallet/createtx_test.go +++ b/wallet/createtx_test.go @@ -7,16 +7,18 @@ import ( "reflect" "sort" "testing" + "time" + "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil/hdkeychain" - "github.com/btcsuite/btcwallet/legacy/txstore" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/walletdb" _ "github.com/btcsuite/btcwallet/walletdb/bdb" + "github.com/btcsuite/btcwallet/wtxmgr" ) // This is a tx that transfers funds (0.371 BTC) to addresses of known privKeys. @@ -80,7 +82,7 @@ func TestCreateTx(t *testing.T) { } // Pick all utxos from txInfo as eligible input. - eligible := eligibleInputsFromTx(t, txInfo.hex, []uint32{1, 2, 3, 4, 5}) + eligible := mockCredits(t, txInfo.hex, []uint32{1, 2, 3, 4, 5}) // Now create a new TX sending 25e6 satoshis to the following addresses: outputs := map[string]btcutil.Amount{outAddr1: 15e6, outAddr2: 10e6} tx, err := createTx(eligible, outputs, bs, defaultFeeIncrement, mgr, account, tstChangeAddress, &chaincfg.TestNet3Params, false) @@ -93,7 +95,7 @@ func TestCreateTx(t *testing.T) { tx.ChangeAddr.String(), changeAddr.String()) } - msgTx := tx.Tx.MsgTx() + msgTx := tx.MsgTx if len(msgTx.TxOut) != 3 { t.Fatalf("Unexpected number of outputs; got %d, want 3", len(msgTx.TxOut)) } @@ -122,7 +124,7 @@ func TestCreateTx(t *testing.T) { func TestCreateTxInsufficientFundsError(t *testing.T) { outputs := map[string]btcutil.Amount{outAddr1: 10, outAddr2: 1e9} - eligible := eligibleInputsFromTx(t, txInfo.hex, []uint32{1}) + eligible := mockCredits(t, txInfo.hex, []uint32{1}) bs := &waddrmgr.BlockStamp{Height: 11111} account := uint32(0) changeAddr, _ := btcutil.DecodeAddress("muqW4gcixv58tVbSKRC5q6CRKy8RmyLgZ5", &chaincfg.TestNet3Params) @@ -207,29 +209,36 @@ func newManager(t *testing.T, privKeys []string, bs *waddrmgr.BlockStamp) *waddr return mgr } -// eligibleInputsFromTx decodes the given txHex and returns the outputs with +// mockCredits decodes the given txHex and returns the outputs with // the given indices as eligible inputs. -func eligibleInputsFromTx(t *testing.T, txHex string, indices []uint32) []txstore.Credit { +func mockCredits(t *testing.T, txHex string, indices []uint32) []wtxmgr.Credit { serialized, err := hex.DecodeString(txHex) if err != nil { t.Fatal(err) } - tx, err := btcutil.NewTxFromBytes(serialized) + utx, err := btcutil.NewTxFromBytes(serialized) if err != nil { t.Fatal(err) } - s := txstore.New("/tmp/tx.bin") - r, err := s.InsertTx(tx, nil) - if err != nil { - t.Fatal(err) + tx := utx.MsgTx() + + isCB := blockchain.IsCoinBaseTx(tx) + now := time.Now() + + eligible := make([]wtxmgr.Credit, len(indices)) + c := wtxmgr.Credit{ + OutPoint: wire.OutPoint{Hash: *utx.Sha()}, + BlockMeta: wtxmgr.BlockMeta{ + Block: wtxmgr.Block{Height: -1}, + }, } - eligible := make([]txstore.Credit, len(indices)) for i, idx := range indices { - credit, err := r.AddCredit(idx, false) - if err != nil { - t.Fatal(err) - } - eligible[i] = credit + c.OutPoint.Index = idx + c.Amount = btcutil.Amount(tx.TxOut[idx].Value) + c.PkScript = tx.TxOut[idx].PkScript + c.Received = now + c.FromCoinBase = isCB + eligible[i] = c } return eligible } diff --git a/wallet/rescan.go b/wallet/rescan.go index 36652a8b62..876f53e9fc 100644 --- a/wallet/rescan.go +++ b/wallet/rescan.go @@ -20,8 +20,8 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcwallet/chain" - "github.com/btcsuite/btcwallet/legacy/txstore" "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wtxmgr" ) // RescanProgressMsg reports the current progress made by a rescan for a @@ -252,10 +252,10 @@ func (w *Wallet) rescanRPCHandler() { // a wallet. This is intended to be used to sync a wallet back up to the // current best block in the main chain, and is considered an initial sync // rescan. -func (w *Wallet) Rescan(addrs []btcutil.Address, unspent []txstore.Credit) error { +func (w *Wallet) Rescan(addrs []btcutil.Address, unspent []wtxmgr.Credit) error { outpoints := make([]*wire.OutPoint, len(unspent)) for i, output := range unspent { - outpoints[i] = output.OutPoint() + outpoints[i] = &output.OutPoint } job := &RescanJob{ diff --git a/wallet/wallet.go b/wallet/wallet.go index 299043b719..a4102b2e6e 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2014 Conformal Systems LLC + * Copyright (c) 2013-2015 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 @@ -32,12 +32,13 @@ import ( "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcwallet/chain" - "github.com/btcsuite/btcwallet/legacy/txstore" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/walletdb" + "github.com/btcsuite/btcwallet/wtxmgr" ) const ( @@ -49,9 +50,10 @@ const ( // the remote chain server. var ErrNotSynced = errors.New("wallet is not synchronized with the chain server") +// Namespace bucket keys. var ( - // waddrmgrNamespaceKey is the namespace key for the waddrmgr package. waddrmgrNamespaceKey = []byte("waddrmgr") + wtxmgrNamespaceKey = []byte("wtxmgr") ) type noopLocker struct{} @@ -66,7 +68,7 @@ type Wallet struct { // Data stores db walletdb.DB Manager *waddrmgr.Manager - TxStore *txstore.Store + TxStore *wtxmgr.Store chainSvr *chain.Client chainSvrLock sync.Locker @@ -101,6 +103,7 @@ type Wallet struct { // calling one of the Listen* methods. connectedBlocks chan waddrmgr.BlockStamp disconnectedBlocks chan waddrmgr.BlockStamp + relevantTxs chan chain.RelevantTx lockStateChanges chan bool // true when locked confirmedBalance chan btcutil.Amount unconfirmedBalance chan btcutil.Amount @@ -114,7 +117,7 @@ type Wallet struct { // newWallet creates a new Wallet structure with the provided address manager // and transaction store. -func newWallet(mgr *waddrmgr.Manager, txs *txstore.Store, db *walletdb.DB) *Wallet { +func newWallet(mgr *waddrmgr.Manager, txs *wtxmgr.Store, db *walletdb.DB) *Wallet { return &Wallet{ db: *db, Manager: mgr, @@ -159,15 +162,6 @@ func (w *Wallet) updateNotificationLock() { w.notificationLock = noopLocker{} } -// CreditAccount returns the first account that can be associated -// with the given credit. -// If no account is found, ErrAccountNotFound is returned. -func (w *Wallet) CreditAccount(c txstore.Credit) (uint32, error) { - _, addrs, _, _ := c.Addresses(w.chainParams) - addr := addrs[0] - return w.Manager.AddrAccount(addr) -} - // ListenConnectedBlocks returns a channel that passes all blocks that a wallet // has been marked in sync with. The channel must be read, or other wallet // methods will block. @@ -254,22 +248,16 @@ func (w *Wallet) ListenUnconfirmedBalance() (<-chan btcutil.Amount, error) { return w.unconfirmedBalance, nil } -// markAddrsUsed marks the addresses credited by the given transaction -// record as used. -func (w *Wallet) markAddrsUsed(t *txstore.TxRecord) error { - for _, c := range t.Credits() { - // Errors don't matter here. If addrs is nil, the - // range below does nothing. - _, addrs, _, _ := c.Addresses(w.chainParams) - for _, addr := range addrs { - addressID := addr.ScriptAddress() - if err := w.Manager.MarkUsed(addressID); err != nil { - return err - } - log.Infof("Marked address used %s", addr.EncodeAddress()) - } +func (w *Wallet) ListenRelevantTxs() (<-chan chain.RelevantTx, error) { + defer w.notificationLock.Unlock() + w.notificationLock.Lock() + + if w.relevantTxs != nil { + return nil, ErrDuplicateListen } - return nil + w.relevantTxs = make(chan chain.RelevantTx) + w.updateNotificationLock() + return w.relevantTxs, nil } func (w *Wallet) notifyConnectedBlock(block waddrmgr.BlockStamp) { @@ -312,6 +300,14 @@ func (w *Wallet) notifyUnconfirmedBalance(bal btcutil.Amount) { w.notificationLock.Unlock() } +func (w *Wallet) notifyRelevantTx(relevantTx chain.RelevantTx) { + w.notificationLock.Lock() + if w.relevantTxs != nil { + w.relevantTxs <- relevantTx + } + w.notificationLock.Unlock() +} + // Start starts the goroutines necessary to manage a wallet. func (w *Wallet) Start(chainServer *chain.Client) { select { @@ -326,8 +322,7 @@ func (w *Wallet) Start(chainServer *chain.Client) { w.chainSvr = chainServer w.chainSvrLock = noopLocker{} - w.wg.Add(7) - go w.diskWriter() + w.wg.Add(6) go w.handleChainNotifications() go w.txCreator() go w.walletLocker() @@ -396,7 +391,7 @@ func (w *Wallet) SetChainSynced(synced bool) { // activeData returns the currently-active receiving addresses and all unspent // outputs. This is primarely intended to provide the parameters for a // rescan request. -func (w *Wallet) activeData() ([]btcutil.Address, []txstore.Credit, error) { +func (w *Wallet) activeData() ([]btcutil.Address, []wtxmgr.Credit, error) { addrs, err := w.Manager.AllActiveAddresses() if err != nil { return nil, nil, err @@ -456,7 +451,6 @@ func (w *Wallet) syncWithChain() error { if err != nil { return err } - w.TxStore.MarkDirty() } break @@ -469,7 +463,7 @@ type ( createTxRequest struct { account uint32 pairs map[string]btcutil.Amount - minconf int + minconf int32 resp chan createTxResponse } createTxResponse struct { @@ -510,7 +504,7 @@ out: // this function is serialized to prevent the creation of many transactions // which spend the same outputs. func (w *Wallet) CreateSimpleTx(account uint32, pairs map[string]btcutil.Amount, - minconf int) (*CreatedTx, error) { + minconf int32) (*CreatedTx, error) { req := createTxRequest{ account: account, @@ -681,40 +675,6 @@ func (w *Wallet) ChangePassphrase(old, new []byte) error { return <-err } -// diskWriter periodically (every 10 seconds) writes out the transaction store -// to disk if it is marked dirty. -func (w *Wallet) diskWriter() { - ticker := time.NewTicker(10 * time.Second) - var wg sync.WaitGroup - var done bool - - for { - select { - case <-ticker.C: - case <-w.quit: - done = true - } - - log.Trace("Writing txstore") - - wg.Add(1) - go func() { - err := w.TxStore.WriteIfDirty() - if err != nil { - log.Errorf("Cannot write txstore: %v", - err) - } - wg.Done() - }() - wg.Wait() - - if done { - break - } - } - w.wg.Done() -} - // AddressUsed returns whether there are any recorded transactions spending to // a given address. Assumming correct TxStore usage, this will return true iff // there are any transactions with outputs to this address in the blockchain or @@ -747,38 +707,45 @@ func (w *Wallet) AccountUsed(account uint32) (bool, error) { // a UTXO must be in a block. If confirmations is 1 or greater, // the balance will be calculated based on how many how many blocks // include a UTXO. -func (w *Wallet) CalculateBalance(confirms int) (btcutil.Amount, error) { +func (w *Wallet) CalculateBalance(confirms int32) (btcutil.Amount, error) { blk := w.Manager.SyncedTo() return w.TxStore.Balance(confirms, blk.Height) } // CalculateAccountBalance sums the amounts of all unspent transaction // outputs to the given account of a wallet and returns the balance. -func (w *Wallet) CalculateAccountBalance(account uint32, confirms int) (btcutil.Amount, error) { +func (w *Wallet) CalculateAccountBalance(account uint32, confirms int32) (btcutil.Amount, error) { var bal btcutil.Amount // Get current block. The block height used for calculating // the number of tx confirmations. - blk := w.Manager.SyncedTo() + syncBlock := w.Manager.SyncedTo() unspent, err := w.TxStore.UnspentOutputs() if err != nil { return 0, err } - for _, c := range unspent { - if c.IsCoinbase() { - if !c.Confirmed(blockchain.CoinbaseMaturity, blk.Height) { - continue - } + for i := range unspent { + output := &unspent[i] + + if !confirmed(confirms, output.Height, syncBlock.Height) { + continue } - if c.Confirmed(confirms, blk.Height) { - creditAccount, err := w.CreditAccount(c) - if err != nil { + if output.FromCoinBase { + const target = blockchain.CoinbaseMaturity + if !confirmed(target, output.Height, syncBlock.Height) { continue } - if creditAccount == account { - bal += c.Amount() - } + } + + var outputAcct uint32 + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + output.PkScript, w.chainParams) + if err == nil && len(addrs) > 0 { + outputAcct, err = w.Manager.AddrAccount(addrs[0]) + } + if err == nil && outputAcct == account { + bal += output.Amount } } return bal, nil @@ -802,35 +769,168 @@ func (w *Wallet) CurrentAddress(account uint32) (btcutil.Address, error) { return addr.Address(), nil } -// ListSinceBlock returns a slice of objects with details about transactions -// since the given block. If the block is -1 then all transactions are included. -// This is intended to be used for listsinceblock RPC replies. -func (w *Wallet) ListSinceBlock(since, curBlockHeight int32, - minconf int) ([]btcjson.ListTransactionsResult, error) { +// CreditCategory describes the type of wallet transaction output. The category +// of "sent transactions" (debits) is always "send", and is not expressed by +// this type. +// +// TODO: This is a requirement of the RPC server and should be moved. +type CreditCategory byte - txList := []btcjson.ListTransactionsResult{} - for _, txRecord := range w.TxStore.Records() { - // Transaction records must only be considered if they occur - // after the block height since. - if since != -1 && txRecord.BlockHeight <= since { - continue +// These constants define the possible credit categories. +const ( + CreditReceive CreditCategory = iota + CreditGenerate + CreditImmature +) + +// String returns the category as a string. This string may be used as the +// JSON string for categories as part of listtransactions and gettransaction +// RPC responses. +func (c CreditCategory) String() string { + switch c { + case CreditReceive: + return "receive" + case CreditGenerate: + return "generate" + case CreditImmature: + return "immature" + default: + return "unknown" + } +} + +// RecvCategory returns the category of received credit outputs from a +// transaction record. The passed block chain height is used to distinguish +// immature from mature coinbase outputs. +// +// TODO: This is intended for use by the RPC server and should be moved out of +// this package at a later time. +func RecvCategory(details *wtxmgr.TxDetails, syncHeight int32) CreditCategory { + if blockchain.IsCoinBaseTx(&details.MsgTx) { + if confirmed(blockchain.CoinbaseMaturity, details.Block.Height, syncHeight) { + return CreditGenerate } + return CreditImmature + } + return CreditReceive +} - // Transactions that have not met minconf confirmations are to - // be ignored. - if !txRecord.Confirmed(minconf, curBlockHeight) { - continue +// ListTransactions creates a object that may be marshalled to a response result +// for a listtransactions RPC. +// +// TODO: This should be moved out of this package into the main package's +// rpcserver.go, along with everything that requires this. +func ListTransactions(details *wtxmgr.TxDetails, syncHeight int32, net *chaincfg.Params) []btcjson.ListTransactionsResult { + var ( + blockHashStr string + blockTime int64 + confirmations int64 = -1 + ) + if details.Block.Height != -1 { + blockHashStr = details.Block.Hash.String() + blockTime = details.Block.Time.Unix() + confirmations = int64(confirms(details.Block.Height, syncHeight)) + } + + results := []btcjson.ListTransactionsResult{} + txHashStr := details.Hash.String() + received := details.Received.Unix() + generated := blockchain.IsCoinBaseTx(&details.MsgTx) + recvCat := RecvCategory(details, syncHeight).String() + + send := len(details.Debits) != 0 + + // Fee can only be determined if every input is a debit. + var feeF64 float64 + if len(details.Debits) == len(details.MsgTx.TxIn) { + var debitTotal btcutil.Amount + for _, deb := range details.Debits { + debitTotal += deb.Amount + } + var outputTotal btcutil.Amount + for _, output := range details.MsgTx.TxOut { + outputTotal += btcutil.Amount(output.Value) } + feeF64 = (debitTotal - outputTotal).ToBTC() + } + +outputs: + for i, output := range details.MsgTx.TxOut { + // Determine if this output is a credit, and if so, determine + // its spentness. + var isCredit bool + var spentCredit bool + for _, cred := range details.Credits { + if cred.Index == uint32(i) { + // Change outputs are ignored. + if cred.Change { + continue outputs + } - jsonResults, err := txRecord.ToJSON(waddrmgr.DefaultAccountName, curBlockHeight, - w.Manager.ChainParams()) - if err != nil { - return nil, err + isCredit = true + spentCredit = cred.Spent + break + } + } + + var address string + _, addrs, _, _ := txscript.ExtractPkScriptAddrs(output.PkScript, net) + if len(addrs) == 1 { + address = addrs[0].EncodeAddress() + } + + amountF64 := btcutil.Amount(output.Value).ToBTC() + result := btcjson.ListTransactionsResult{ + Address: address, + Generated: generated, + TxID: txHashStr, + Time: received, + TimeReceived: received, + WalletConflicts: []string{}, + BlockHash: blockHashStr, + BlockTime: blockTime, + Confirmations: confirmations, + } + + // Add a received/generated/immature result if this is a credit. + // If the output was spent, create a second result under the + // send category with the inverse of the output amount. It is + // therefore possible that a single output may be included in + // the results set zero, one, or two times. + // + // Since credits are not saved for outputs that are not + // controlled by this wallet, all non-credits from transactions + // with debits are grouped under the send category. + + if send || spentCredit { + result.Category = "send" + result.Amount = -amountF64 + result.Fee = feeF64 + results = append(results, result) + } + if isCredit { + result.Category = recvCat + result.Amount = amountF64 + results = append(results, result) } - txList = append(txList, jsonResults...) } + return results +} - return txList, nil +// ListSinceBlock returns a slice of objects with details about transactions +// since the given block. If the block is -1 then all transactions are included. +// This is intended to be used for listsinceblock RPC replies. +func (w *Wallet) ListSinceBlock(start, end, syncHeight int32) ([]btcjson.ListTransactionsResult, error) { + txList := []btcjson.ListTransactionsResult{} + err := w.TxStore.RangeTransactions(start, end, func(details []wtxmgr.TxDetails) (bool, error) { + for _, detail := range details { + jsonResults := ListTransactions(&detail, syncHeight, + w.chainParams) + txList = append(txList, jsonResults...) + } + return false, nil + }) + return txList, err } // ListTransactions returns a slice of objects with details about a recorded @@ -841,21 +941,40 @@ func (w *Wallet) ListTransactions(from, count int) ([]btcjson.ListTransactionsRe // Get current block. The block height used for calculating // the number of tx confirmations. - blk := w.Manager.SyncedTo() + syncBlock := w.Manager.SyncedTo() + + // Need to skip the first from transactions, and after those, only + // include the next count transactions. + skipped := 0 + n := 0 + + // Return newer results first by starting at mempool height and working + // down to the genesis block. + err := w.TxStore.RangeTransactions(-1, 0, func(details []wtxmgr.TxDetails) (bool, error) { + // Iterate over transactions at this height in reverse order. + // This does nothing for unmined transactions, which are + // unsorted, but it will process mined transactions in the + // reverse order they were marked mined. + for i := len(details) - 1; i >= 0; i-- { + if from > skipped { + skipped++ + continue + } - records := w.TxStore.Records() - lastLookupIdx := len(records) - count - // Search in reverse order: lookup most recently-added first. - for i := len(records) - 1; i >= from && i >= lastLookupIdx; i-- { - jsonResults, err := records[i].ToJSON(waddrmgr.DefaultAccountName, blk.Height, - w.Manager.ChainParams()) - if err != nil { - return nil, err + n++ + if n > count { + return true, nil + } + + jsonResults := ListTransactions(&details[i], + syncBlock.Height, w.chainParams) + txList = append(txList, jsonResults...) } - txList = append(txList, jsonResults...) - } - return txList, nil + return false, nil + }) + + return txList, err } // ListAddressTransactions returns a slice of objects with details about @@ -868,34 +987,42 @@ func (w *Wallet) ListAddressTransactions(pkHashes map[string]struct{}) ( // Get current block. The block height used for calculating // the number of tx confirmations. - blk := w.Manager.SyncedTo() - - for _, r := range w.TxStore.Records() { - for _, c := range r.Credits() { - // We only care about the case where len(addrs) == 1, - // and err will never be non-nil in that case. - _, addrs, _, _ := c.Addresses(w.chainParams) - if len(addrs) != 1 { - continue - } - apkh, ok := addrs[0].(*btcutil.AddressPubKeyHash) - if !ok { - continue - } + syncBlock := w.Manager.SyncedTo() + + err := w.TxStore.RangeTransactions(0, -1, func(details []wtxmgr.TxDetails) (bool, error) { + loopDetails: + for i := range details { + detail := &details[i] + + for _, cred := range detail.Credits { + pkScript := detail.MsgTx.TxOut[cred.Index].PkScript + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + pkScript, w.chainParams) + if err != nil || len(addrs) != 1 { + continue + } + apkh, ok := addrs[0].(*btcutil.AddressPubKeyHash) + if !ok { + continue + } + _, ok = pkHashes[string(apkh.ScriptAddress())] + if !ok { + continue + } - if _, ok := pkHashes[string(apkh.ScriptAddress())]; !ok { - continue - } - jsonResult, err := c.ToJSON(waddrmgr.DefaultAccountName, blk.Height, - w.Manager.ChainParams()) - if err != nil { - return nil, err + jsonResults := ListTransactions(detail, + syncBlock.Height, w.chainParams) + if err != nil { + return false, err + } + txList = append(txList, jsonResults...) + continue loopDetails } - txList = append(txList, jsonResult) } - } + return false, nil + }) - return txList, nil + return txList, err } // ListAllTransactions returns a slice of objects with details about a recorded @@ -906,20 +1033,61 @@ func (w *Wallet) ListAllTransactions() ([]btcjson.ListTransactionsResult, error) // Get current block. The block height used for calculating // the number of tx confirmations. - blk := w.Manager.SyncedTo() - - // Search in reverse order: lookup most recently-added first. - records := w.TxStore.Records() - for i := len(records) - 1; i >= 0; i-- { - jsonResults, err := records[i].ToJSON(waddrmgr.DefaultAccountName, blk.Height, - w.Manager.ChainParams()) - if err != nil { - return nil, err + syncBlock := w.Manager.SyncedTo() + + // Return newer results first by starting at mempool height and working + // down to the genesis block. + err := w.TxStore.RangeTransactions(-1, 0, func(details []wtxmgr.TxDetails) (bool, error) { + // Iterate over transactions at this height in reverse order. + // This does nothing for unmined transactions, which are + // unsorted, but it will process mined transactions in the + // reverse order they were marked mined. + for i := len(details) - 1; i >= 0; i-- { + jsonResults := ListTransactions(&details[i], + syncBlock.Height, w.chainParams) + txList = append(txList, jsonResults...) } - txList = append(txList, jsonResults...) + return false, nil + }) + + return txList, err +} + +// creditSlice satisifies the sort.Interface interface to provide sorting +// transaction credits from oldest to newest. Credits with the same receive +// time and mined in the same block are not guaranteed to be sorted by the order +// they appear in the block. Credits from the same transaction are sorted by +// output index. +type creditSlice []wtxmgr.Credit + +func (s creditSlice) Len() int { + return len(s) +} + +func (s creditSlice) Less(i, j int) bool { + switch { + // If both credits are from the same tx, sort by output index. + case s[i].OutPoint.Hash == s[j].OutPoint.Hash: + return s[i].OutPoint.Index < s[j].OutPoint.Index + + // If both transactions are unmined, sort by their received date. + case s[i].Height == -1 && s[j].Height == -1: + return s[i].Received.Before(s[j].Received) + + // Unmined (newer) txs always come last. + case s[i].Height == -1: + return false + case s[j].Height == -1: + return true + + // If both txs are mined in different blocks, sort by block height. + default: + return s[i].Height < s[j].Height } +} - return txList, nil +func (s creditSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] } // ListUnspent returns a slice of objects representing the unspent wallet @@ -927,44 +1095,65 @@ func (w *Wallet) ListAllTransactions() ([]btcjson.ListTransactionsResult, error) // minconf, less than maxconf and if addresses is populated only the addresses // contained within it will be considered. If we know nothing about a // transaction an empty array will be returned. -func (w *Wallet) ListUnspent(minconf, maxconf int, +func (w *Wallet) ListUnspent(minconf, maxconf int32, addresses map[string]bool) ([]*btcjson.ListUnspentResult, error) { - results := []*btcjson.ListUnspentResult{} - - blk := w.Manager.SyncedTo() + syncBlock := w.Manager.SyncedTo() filter := len(addresses) != 0 - unspent, err := w.TxStore.SortedUnspentOutputs() + unspent, err := w.TxStore.UnspentOutputs() if err != nil { return nil, err } + sort.Sort(sort.Reverse(creditSlice(unspent))) + + results := make([]*btcjson.ListUnspentResult, 0, len(unspent)) + for i := range unspent { + output := &unspent[i] - for _, credit := range unspent { - confs := credit.Confirmations(blk.Height) - if int(confs) < minconf || int(confs) > maxconf { + // Outputs with fewer confirmations than the minimum or more + // confs than the maximum are excluded. + confs := confirms(output.Height, syncBlock.Height) + if confs < minconf || confs > maxconf { continue } - if credit.IsCoinbase() { - if !credit.Confirmed(blockchain.CoinbaseMaturity, blk.Height) { + + // Only mature coinbase outputs are included. + if output.FromCoinBase { + const target = blockchain.CoinbaseMaturity + if !confirmed(target, output.Height, syncBlock.Height) { continue } } - if w.LockedOutpoint(*credit.OutPoint()) { + + // Exclude locked outputs from the result set. + if w.LockedOutpoint(output.OutPoint) { continue } - creditAccount, err := w.CreditAccount(credit) + // Lookup the associated account for the output. Use the + // default account name in case there is no associated account + // for some reason, although this should never happen. + // + // This will be unnecessary once transactions and outputs are + // grouped under the associated account in the db. + acctName := waddrmgr.DefaultAccountName + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + output.PkScript, w.chainParams) if err != nil { continue } - accountName, err := w.Manager.AccountName(creditAccount) - if err != nil { - return nil, err + if len(addrs) > 0 { + acct, err := w.Manager.AddrAccount(addrs[0]) + if err == nil { + s, err := w.Manager.AccountName(acct) + if err == nil { + acctName = s + } + } } - _, addrs, _, _ := credit.Addresses(w.chainParams) if filter { for _, addr := range addrs { _, ok := addresses[addr.EncodeAddress()] @@ -976,11 +1165,11 @@ func (w *Wallet) ListUnspent(minconf, maxconf int, } include: result := &btcjson.ListUnspentResult{ - TxId: credit.Tx().Sha().String(), - Vout: credit.OutputIndex, - Account: accountName, - ScriptPubKey: hex.EncodeToString(credit.TxOut().PkScript), - Amount: credit.Amount().ToBTC(), + TxId: output.OutPoint.Hash.String(), + Vout: output.OutPoint.Index, + Account: acctName, + ScriptPubKey: hex.EncodeToString(output.PkScript), + Amount: output.Amount.ToBTC(), Confirmations: int64(confs), } @@ -1098,11 +1287,11 @@ func (w *Wallet) ImportPrivateKey(wif *btcutil.WIF, bs *waddrmgr.BlockStamp, } // ExportWatchingWallet returns a watching-only version of the wallet serialized -// in a map. -func (w *Wallet) ExportWatchingWallet(pubPass string) (map[string]string, error) { +// database as a base64-encoded string. +func (w *Wallet) ExportWatchingWallet(pubPass string) (string, error) { tmpDir, err := ioutil.TempDir("", "btcwallet") if err != nil { - return nil, err + return "", err } defer os.RemoveAll(tmpDir) @@ -1110,11 +1299,11 @@ func (w *Wallet) ExportWatchingWallet(pubPass string) (map[string]string, error) woDbPath := filepath.Join(tmpDir, walletDbWatchingOnlyName) fi, err := os.OpenFile(woDbPath, os.O_CREATE|os.O_RDWR, 0600) if err != nil { - return nil, err + return "", err } if err := w.db.Copy(fi); err != nil { fi.Close() - return nil, err + return "", err } fi.Close() defer os.Remove(woDbPath) @@ -1124,18 +1313,18 @@ func (w *Wallet) ExportWatchingWallet(pubPass string) (map[string]string, error) woDb, err := walletdb.Open("bdb", woDbPath) if err != nil { _ = os.Remove(woDbPath) - return nil, err + return "", err } defer woDb.Close() namespace, err := woDb.Namespace(waddrmgrNamespaceKey) if err != nil { - return nil, err + return "", err } woMgr, err := waddrmgr.Open(namespace, []byte(pubPass), w.chainParams, nil) if err != nil { - return nil, err + return "", err } defer woMgr.Close() @@ -1146,7 +1335,7 @@ func (w *Wallet) ExportWatchingWallet(pubPass string) (map[string]string, error) // just falls through to the export below. if merr, ok := err.(waddrmgr.ManagerError); ok && merr.ErrorCode != waddrmgr.ErrWatchingOnly { - return nil, err + return "", err } } @@ -1157,25 +1346,14 @@ func (w *Wallet) ExportWatchingWallet(pubPass string) (map[string]string, error) return woWallet.exportBase64() } -// exportBase64 exports a wallet's serialized database and tx store as -// base64-encoded values in a map. -func (w *Wallet) exportBase64() (map[string]string, error) { +// exportBase64 exports a wallet's serialized database as a base64-encoded +// string. +func (w *Wallet) exportBase64() (string, error) { var buf bytes.Buffer - m := make(map[string]string) - if err := w.db.Copy(&buf); err != nil { - return nil, err - } - m["wallet"] = base64.StdEncoding.EncodeToString(buf.Bytes()) - buf.Reset() - - if _, err := w.TxStore.WriteTo(&buf); err != nil { - return nil, err + return "", err } - m["tx"] = base64.StdEncoding.EncodeToString(buf.Bytes()) - buf.Reset() - - return m, nil + return base64.StdEncoding.EncodeToString(buf.Bytes()), nil } // LockedOutpoint returns whether an outpoint has been marked as locked and @@ -1223,17 +1401,22 @@ func (w *Wallet) LockedOutpoints() []btcjson.TransactionInput { // credits that are not known to have been mined into a block, and attempts // to send each to the chain server for relay. func (w *Wallet) ResendUnminedTxs() { - txs := w.TxStore.UnminedDebitTxs() + txs, err := w.TxStore.UnminedTxs() + if err != nil { + log.Errorf("Cannot load unmined transactions for resending: %v", err) + return + } for _, tx := range txs { - _, err := w.chainSvr.SendRawTransaction(tx.MsgTx(), false) + resp, err := w.chainSvr.SendRawTransaction(tx, false) if err != nil { // TODO(jrick): Check error for if this tx is a double spend, // remove it if so. + txHash, _ := tx.TxSha() log.Debugf("Could not resend transaction %v: %v", - tx.Sha(), err) + txHash, err) continue } - log.Debugf("Resent unmined transaction %v", tx.Sha()) + log.Debugf("Resent unmined transaction %v", resp) } } @@ -1295,78 +1478,104 @@ func (w *Wallet) NewChangeAddress(account uint32) (btcutil.Address, error) { return utilAddrs[0], nil } +// confirmed checks whether a transaction at height txHeight has met minconf +// confirmations for a blockchain at height curHeight. +func confirmed(minconf, txHeight, curHeight int32) bool { + return confirms(txHeight, curHeight) >= minconf +} + +// confirms returns the number of confirmations for a transaction in a block at +// height txHeight (or -1 for an unconfirmed tx) given the chain height +// curHeight. +func confirms(txHeight, curHeight int32) int32 { + switch { + case txHeight == -1, txHeight > curHeight: + return 0 + default: + return curHeight - txHeight + 1 + } +} + // TotalReceivedForAccount iterates through a wallet's transaction history, // returning the total amount of bitcoins received for a single wallet // account. -func (w *Wallet) TotalReceivedForAccount(account uint32, confirms int) (btcutil.Amount, uint64, error) { - blk := w.Manager.SyncedTo() - - // Number of confirmations of the last transaction. - var confirmations uint64 - - var amount btcutil.Amount - for _, r := range w.TxStore.Records() { - for _, c := range r.Credits() { - if !c.Confirmed(confirms, blk.Height) { - // Not enough confirmations, skip the current block. - continue - } - creditAccount, err := w.CreditAccount(c) - if err != nil { - continue - } - if creditAccount == account { - amount += c.Amount() - confirmations = uint64(c.Confirmations(blk.Height)) - break +func (w *Wallet) TotalReceivedForAccount(account uint32, minConf int32) (btcutil.Amount, int32, error) { + syncBlock := w.Manager.SyncedTo() + + var ( + amount btcutil.Amount + lastConf int32 // Confs of the last matching transaction. + stopHeight int32 + ) + + if minConf > 0 { + stopHeight = syncBlock.Height - minConf + 1 + } else { + stopHeight = -1 + } + err := w.TxStore.RangeTransactions(0, stopHeight, func(details []wtxmgr.TxDetails) (bool, error) { + for i := range details { + detail := &details[i] + for _, cred := range detail.Credits { + pkScript := detail.MsgTx.TxOut[cred.Index].PkScript + var outputAcct uint32 + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + pkScript, w.chainParams) + if err == nil && len(addrs) > 0 { + outputAcct, err = w.Manager.AddrAccount(addrs[0]) + } + if err == nil && outputAcct == account { + amount += cred.Amount + lastConf = confirms(detail.Block.Height, syncBlock.Height) + } } } - } + return false, nil + }) - return amount, confirmations, nil + return amount, lastConf, err } // TotalReceivedForAddr iterates through a wallet's transaction history, // returning the total amount of bitcoins received for a single wallet // address. -func (w *Wallet) TotalReceivedForAddr(addr btcutil.Address, confirms int) (btcutil.Amount, error) { - blk := w.Manager.SyncedTo() - - addrStr := addr.EncodeAddress() - var amount btcutil.Amount - for _, r := range w.TxStore.Records() { - for _, c := range r.Credits() { - if !c.Confirmed(confirms, blk.Height) { - continue - } - - _, addrs, _, err := c.Addresses(w.chainParams) - // An error creating addresses from the output script only - // indicates a non-standard script, so ignore this credit. - if err != nil { - continue - } - for _, a := range addrs { - if addrStr == a.EncodeAddress() { - amount += c.Amount() - break +func (w *Wallet) TotalReceivedForAddr(addr btcutil.Address, minConf int32) (btcutil.Amount, error) { + syncBlock := w.Manager.SyncedTo() + + var ( + addrStr = addr.EncodeAddress() + amount btcutil.Amount + stopHeight int32 + ) + + if minConf > 0 { + stopHeight = syncBlock.Height - minConf + 1 + } else { + stopHeight = -1 + } + err := w.TxStore.RangeTransactions(0, stopHeight, func(details []wtxmgr.TxDetails) (bool, error) { + for i := range details { + detail := &details[i] + for _, cred := range detail.Credits { + pkScript := detail.MsgTx.TxOut[cred.Index].PkScript + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + pkScript, w.chainParams) + // An error creating addresses from the output script only + // indicates a non-standard script, so ignore this credit. + if err != nil { + continue + } + for _, a := range addrs { + if addrStr == a.EncodeAddress() { + amount += cred.Amount + break + } } } } - } - - return amount, nil -} - -// TxRecord iterates through all transaction records saved in the store, -// returning the first with an equivalent transaction hash. -func (w *Wallet) TxRecord(txSha *wire.ShaHash) (r *txstore.TxRecord, ok bool) { - for _, r = range w.TxStore.Records() { - if *r.Tx().Sha() == *txSha { - return r, true - } - } - return nil, false + return false, nil + }) + return amount, err } // Db returns wallet db being used by a wallet diff --git a/walletsetup.go b/walletsetup.go index 6c90bbbabd..edea21785d 100644 --- a/walletsetup.go +++ b/walletsetup.go @@ -31,17 +31,18 @@ import ( "github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil/hdkeychain" "github.com/btcsuite/btcwallet/legacy/keystore" - "github.com/btcsuite/btcwallet/legacy/txstore" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/walletdb" _ "github.com/btcsuite/btcwallet/walletdb/bdb" + "github.com/btcsuite/btcwallet/wtxmgr" "github.com/btcsuite/golangcrypto/ssh/terminal" ) +// Namespace keys var ( - // waddrmgrNamespaceKey is the namespace key for the waddrmgr package. waddrmgrNamespaceKey = []byte("waddrmgr") + wtxmgrNamespaceKey = []byte("wtxmgr") ) // networkDir returns the directory name of a network directory to hold wallet @@ -560,7 +561,7 @@ func createSimulationWallet(cfg *config) error { // openDb opens and returns a *walletdb.DB (boltdb here) given the // directory and dbname -func openDb(directory string, dbname string) (*walletdb.DB, error) { +func openDb(directory string, dbname string) (walletdb.DB, error) { dbPath := filepath.Join(directory, dbname) // Ensure that the network directory exists. @@ -569,21 +570,15 @@ func openDb(directory string, dbname string) (*walletdb.DB, error) { } // Open the database using the boltdb backend. - db, err := walletdb.Open("bdb", dbPath) - if err != nil { - return nil, err - } - return &db, nil + return walletdb.Open("bdb", dbPath) } // openWaddrmgr returns an address manager given a database, namespace, // public pass and the chain params // It prompts for seed and private passphrase required in case of upgrades -func openWaddrmgr(db *walletdb.DB, namespaceKey []byte, pass string, - chainParams *chaincfg.Params) (*waddrmgr.Manager, error) { - +func openWaddrmgr(db walletdb.DB, pass string) (*waddrmgr.Manager, error) { // Get the namespace for the address manager. - namespace, err := (*db).Namespace(namespaceKey) + namespace, err := db.Namespace(waddrmgrNamespaceKey) if err != nil { return nil, err } @@ -592,10 +587,16 @@ func openWaddrmgr(db *walletdb.DB, namespaceKey []byte, pass string, ObtainSeed: promptSeed, ObtainPrivatePass: promptPrivPassPhrase, } - // Open address manager and transaction store. - // var txs *txstore.Store - return waddrmgr.Open(namespace, []byte(pass), - chainParams, config) + return waddrmgr.Open(namespace, []byte(pass), activeNet.Params, config) +} + +func openWtxmgr(db walletdb.DB) (*wtxmgr.Store, error) { + // Get the namespace for the address manager. + namespace, err := db.Namespace(wtxmgrNamespaceKey) + if err != nil { + return nil, err + } + return wtxmgr.Open(namespace) } // openWallet returns a wallet. The function handles opening an existing wallet @@ -610,35 +611,17 @@ func openWallet() (*wallet.Wallet, error) { return nil, err } - var txs *txstore.Store - mgr, err := openWaddrmgr(db, waddrmgrNamespaceKey, cfg.WalletPass, - activeNet.Params) - if err == nil { - txs, err = txstore.OpenDir(netdir) + mgr, err := openWaddrmgr(db, cfg.WalletPass) + if err != nil { + return nil, err } + txs, err := openWtxmgr(db) if err != nil { - // Special case: if the address manager was successfully read - // (mgr != nil) but the transaction store was not, create a - // new txstore and write it out to disk. Write an unsynced - // manager back to disk so on future opens, the empty txstore - // is not considered fully synced. - if mgr == nil { - log.Errorf("%v", err) - return nil, err - } - - txs = txstore.New(netdir) - txs.MarkDirty() - err = txs.WriteIfDirty() - if err != nil { - log.Errorf("%v", err) - return nil, err - } - mgr.SetSyncedTo(nil) + return nil, err } walletConfig := &wallet.Config{ - Db: db, + Db: &db, // TODO: Remove the pointer TxStore: txs, Waddrmgr: mgr, ChainParams: activeNet.Params,