632148ed55
This commit is the result of inspecting the results of both cpu and memory profiling, to improve areas where wallet can be more efficient on transaction inserts. One problem that's very evident by profiling is how much waiting there is for file (txstore, wallet) writes. This commit does not attempt to fix this yet, but focuses on the easier-to-fix memory allocation issues which can slow down the rest of wallet due to excessive garbage collection scanning. While here, fix a race where a closure run as a goroutine was closing over a range iterator.
1351 lines
40 KiB
Go
1351 lines
40 KiB
Go
/*
|
|
* Copyright (c) 2013, 2014 Conformal Systems LLC <info@conformal.com>
|
|
*
|
|
* Permission to use, copy, modify, and distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice appear in all copies.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
|
|
package txstore
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/conformal/btcchain"
|
|
"github.com/conformal/btcnet"
|
|
"github.com/conformal/btcscript"
|
|
"github.com/conformal/btcutil"
|
|
"github.com/conformal/btcwire"
|
|
)
|
|
|
|
var (
|
|
// ErrDuplicateInsert describes the error where an insert was ignored
|
|
// for being a duplicate.
|
|
ErrDuplicateInsert = errors.New("duplicate insert")
|
|
|
|
// ErrInconsistentStore describes an error where the transaction store
|
|
// is in an inconsistent state. This error is unrecoverable, and the
|
|
// transaction store should be regenerated by a rescan.
|
|
ErrInconsistentStore = errors.New("inconsistant transaction store")
|
|
|
|
// ErrUnsupportedVersion represents an error where a serialized
|
|
// object is marked with a version that is no longer supported
|
|
// during deserialization.
|
|
ErrUnsupportedVersion = errors.New("version no longer supported")
|
|
)
|
|
|
|
// MissingValueError is a catch-all error interface for any error due to a
|
|
// missing value in the transaction store.
|
|
type MissingValueError interface {
|
|
Error() string
|
|
MissingValueError() error
|
|
}
|
|
|
|
// MissingBlockError describes an error where a block could not be found in
|
|
// the transaction store. The value is the height of the missing block.
|
|
type MissingBlockError int32
|
|
|
|
// Error implements the error interface.
|
|
func (e MissingBlockError) Error() string {
|
|
return fmt.Sprintf("missing record for block %d", e)
|
|
}
|
|
|
|
// MissingValueError implements the MissingValueError interface.
|
|
func (e MissingBlockError) MissingValueError() error {
|
|
return e
|
|
}
|
|
|
|
// MissingBlockTxError describes an error where a mined transaction could
|
|
// not be found in the transaction store. The value is the lookup key for
|
|
// the mined transaction.
|
|
type MissingBlockTxError BlockTxKey
|
|
|
|
// Error implements the error interface.
|
|
func (e MissingBlockTxError) Error() string {
|
|
return fmt.Sprintf("missing record for transaction at block %d index "+
|
|
"%d", e.BlockHeight, e.BlockIndex)
|
|
}
|
|
|
|
// MissingValueError implements the MissingValueError interface.
|
|
func (e MissingBlockTxError) MissingValueError() error {
|
|
return e
|
|
}
|
|
|
|
// MissingDebitsError describes an error where a mined transaction does not
|
|
// contain any debiting records. The value is the lookup key for the mined
|
|
// transaction.
|
|
type MissingDebitsError BlockTxKey
|
|
|
|
// Error implements the error interface.
|
|
func (e MissingDebitsError) Error() string {
|
|
return fmt.Sprintf("missing record for debits at block %d index %d",
|
|
e.BlockHeight, e.BlockIndex)
|
|
}
|
|
|
|
// MissingValueError implements the MissingValueError interface.
|
|
func (e MissingDebitsError) MissingValueError() error {
|
|
return e
|
|
}
|
|
|
|
// MissingCreditError describes an error where a mined transaction does not
|
|
// contain a credit record at for some output. The value is the lookup key
|
|
// for the mined transaction's output.
|
|
type MissingCreditError BlockOutputKey
|
|
|
|
// Error implements the error interface.
|
|
func (e MissingCreditError) Error() string {
|
|
return fmt.Sprintf("missing record for received transaction output at "+
|
|
"block %d index %d output %d", e.BlockHeight, e.BlockIndex,
|
|
e.OutputIndex)
|
|
}
|
|
|
|
// MissingValueError implements the MissingValueError interface.
|
|
func (e MissingCreditError) MissingValueError() error {
|
|
return e
|
|
}
|
|
|
|
// TxRecord is the record type for all transactions in the store. If the
|
|
// transaction is mined, BlockTxKey will be the lookup key for the transaction.
|
|
// Otherwise, the embedded BlockHeight will be -1.
|
|
type TxRecord struct {
|
|
BlockTxKey
|
|
*txRecord
|
|
s *Store
|
|
}
|
|
|
|
// Block holds details about a block that contains wallet transactions.
|
|
type Block struct {
|
|
Hash btcwire.ShaHash
|
|
Time time.Time
|
|
Height int32
|
|
}
|
|
|
|
// Debits is the type representing any TxRecord which debits from previous
|
|
// wallet transaction outputs.
|
|
type Debits struct {
|
|
*TxRecord
|
|
}
|
|
|
|
// Credit is the type representing a transaction output which was spent or
|
|
// is still spendable by wallet. A UTXO is an unspent Credit, but not all
|
|
// Credits are UTXOs.
|
|
type Credit struct {
|
|
*TxRecord
|
|
OutputIndex uint32
|
|
}
|
|
|
|
// blockAmounts holds the immediately spendable and reward (coinbase) amounts
|
|
// as a result of all transactions in a block.
|
|
type blockAmounts struct {
|
|
Spendable btcutil.Amount
|
|
Reward btcutil.Amount
|
|
}
|
|
|
|
// Store implements a transaction store for storing and managing wallet
|
|
// transactions.
|
|
type Store struct {
|
|
// blocks holds wallet transaction records for each block they appear
|
|
// in. This is sorted by block height in increasing order. A separate
|
|
// map is included to lookup indexes for blocks at some height.
|
|
blocks []*blockTxCollection
|
|
blockIndexes map[int32]uint32
|
|
|
|
unspent map[btcwire.OutPoint]BlockTxKey
|
|
|
|
// unconfirmed holds a collection of wallet transactions that have not
|
|
// been mined into a block yet.
|
|
unconfirmed unconfirmedStore
|
|
}
|
|
|
|
// blockTxCollection holds a collection of wallet transactions from exactly one
|
|
// block.
|
|
type blockTxCollection struct {
|
|
// Block holds the hash, time, and height of the block.
|
|
Block
|
|
|
|
// amountDeltas is the net increase or decrease of BTC controllable by
|
|
// wallet addresses due to transactions in this block. This value only
|
|
// tracks the total amount, not amounts for individual addresses (which
|
|
// this txstore implementation is not aware of).
|
|
amountDeltas blockAmounts
|
|
|
|
// txs holds all transaction records for a block, sorted by block index.
|
|
// A separate map is included to lookup indexes for txs at some block
|
|
// index.
|
|
txs []*txRecord
|
|
txIndexes map[int]uint32
|
|
}
|
|
|
|
// unconfirmedStore stores all unconfirmed transactions managed by the Store.
|
|
type unconfirmedStore struct {
|
|
// txs contains all unconfirmed transactions, mapping from their
|
|
// transaction hash to the records.
|
|
txs map[btcwire.ShaHash]*txRecord
|
|
|
|
// spentBlockOutPoints maps from spent outputs from mined transaction
|
|
// to the unconfirmed transaction which spends it. An additional
|
|
// map is included to lookup the output key by its outpoint.
|
|
spentBlockOutPoints map[BlockOutputKey]*txRecord
|
|
spentBlockOutPointKeys map[btcwire.OutPoint]BlockOutputKey
|
|
|
|
// spentUnconfirmed maps from an unconfirmed outpoint to the unconfirmed
|
|
// transaction which spends it.
|
|
spentUnconfirmed map[btcwire.OutPoint]*txRecord
|
|
|
|
// previousOutpoints maps all previous outputs to the transaction record
|
|
// of the unconfirmed transaction which spends it. This is primarly
|
|
// designed to assist with double spend detection without iterating
|
|
// through each value of the txs map.
|
|
previousOutpoints map[btcwire.OutPoint]*txRecord
|
|
}
|
|
|
|
// BlockTxKey is a lookup key for a single mined transaction in the store.
|
|
type BlockTxKey struct {
|
|
BlockIndex int
|
|
BlockHeight int32
|
|
}
|
|
|
|
// BlockOutputKey is a lookup key for a transaction output from a block in the
|
|
// store.
|
|
type BlockOutputKey struct {
|
|
BlockTxKey
|
|
OutputIndex uint32
|
|
}
|
|
|
|
// txRecord holds all credits and debits created by a transaction's inputs and
|
|
// outputs.
|
|
type txRecord struct {
|
|
// tx is the transaction that all records in this structure reference.
|
|
tx *btcutil.Tx
|
|
|
|
// debit records the debits this transaction creates, or nil if the
|
|
// transaction does not spend any previous credits.
|
|
debits *debits
|
|
|
|
// credits holds all records for received transaction outputs from
|
|
// this transaction.
|
|
credits []*credit
|
|
|
|
received time.Time
|
|
}
|
|
|
|
// debits records the debits a transaction record makes from previous wallet
|
|
// transaction credits.
|
|
type debits struct {
|
|
amount btcutil.Amount
|
|
spends []BlockOutputKey
|
|
}
|
|
|
|
// credit describes a transaction output which was or is spendable by wallet.
|
|
type credit struct {
|
|
change bool
|
|
locked bool
|
|
spentBy *BlockTxKey // nil if unspent
|
|
}
|
|
|
|
// New allocates and initializes a new transaction store.
|
|
func New() *Store {
|
|
return &Store{
|
|
blockIndexes: map[int32]uint32{},
|
|
unspent: map[btcwire.OutPoint]BlockTxKey{},
|
|
unconfirmed: unconfirmedStore{
|
|
txs: map[btcwire.ShaHash]*txRecord{},
|
|
spentBlockOutPoints: map[BlockOutputKey]*txRecord{},
|
|
spentBlockOutPointKeys: map[btcwire.OutPoint]BlockOutputKey{},
|
|
spentUnconfirmed: map[btcwire.OutPoint]*txRecord{},
|
|
previousOutpoints: map[btcwire.OutPoint]*txRecord{},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (s *Store) lookupBlock(height int32) (*blockTxCollection, error) {
|
|
if i, ok := s.blockIndexes[height]; ok {
|
|
return s.blocks[i], nil
|
|
}
|
|
return nil, MissingBlockError(height)
|
|
}
|
|
|
|
func (s *Store) lookupBlockTx(key BlockTxKey) (*txRecord, error) {
|
|
coll, err := s.lookupBlock(key.BlockHeight)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if i, ok := coll.txIndexes[key.BlockIndex]; ok {
|
|
return coll.txs[i], nil
|
|
}
|
|
return nil, MissingBlockTxError{key.BlockIndex, key.BlockHeight}
|
|
}
|
|
|
|
func (s *Store) lookupBlockDebits(key BlockTxKey) (*debits, error) {
|
|
r, err := s.lookupBlockTx(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if r.debits == nil {
|
|
return nil, MissingDebitsError(key)
|
|
}
|
|
return r.debits, nil
|
|
}
|
|
|
|
func (r *txRecord) lookupBlockCredit(key BlockOutputKey) (*credit, error) {
|
|
switch {
|
|
case len(r.credits) <= int(key.OutputIndex):
|
|
fallthrough
|
|
case r.credits[key.OutputIndex] == nil:
|
|
return nil, MissingCreditError(key)
|
|
}
|
|
return r.credits[key.OutputIndex], nil
|
|
}
|
|
|
|
func (s *Store) lookupBlockCredit(key BlockOutputKey) (*credit, error) {
|
|
txRecord, err := s.lookupBlockTx(key.BlockTxKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return txRecord.lookupBlockCredit(key)
|
|
}
|
|
|
|
func (s *Store) blockCollectionForInserts(block *Block) *blockTxCollection {
|
|
b, err := s.lookupBlock(block.Height)
|
|
switch e := err.(type) {
|
|
case MissingBlockError:
|
|
b = &blockTxCollection{
|
|
Block: *block,
|
|
txIndexes: map[int]uint32{},
|
|
}
|
|
|
|
// If this new block cannot be appended to the end of the blocks
|
|
// slice (which would disobey ordering blocks by their height),
|
|
// reslice and update the store's map of block heights to block
|
|
// slice indexes.
|
|
if len(s.blocks) > 0 && s.blocks[len(s.blocks)-1].Height > block.Height {
|
|
i := uint32(len(s.blocks))
|
|
for i != 0 && s.blocks[i-1].Height >= block.Height {
|
|
i--
|
|
}
|
|
detached := s.blocks[i:]
|
|
s.blocks = append(s.blocks[:i], b)
|
|
s.blockIndexes[b.Height] = i
|
|
for i, b := range detached {
|
|
newIndex := uint32(i + len(s.blocks))
|
|
s.blockIndexes[b.Height] = newIndex
|
|
}
|
|
s.blocks = append(s.blocks, detached...)
|
|
|
|
} else {
|
|
s.blockIndexes[int32(e)] = uint32(len(s.blocks))
|
|
s.blocks = append(s.blocks, b)
|
|
}
|
|
}
|
|
return b
|
|
}
|
|
|
|
func (c *blockTxCollection) lookupTxRecord(blockIndex int) (*txRecord, uint32, error) {
|
|
if i, ok := c.txIndexes[blockIndex]; ok {
|
|
return c.txs[i], i, nil
|
|
}
|
|
return nil, 0, MissingBlockTxError{blockIndex, c.Block.Height}
|
|
}
|
|
|
|
func (c *blockTxCollection) txRecordForInserts(tx *btcutil.Tx) *txRecord {
|
|
if i, ok := c.txIndexes[tx.Index()]; ok {
|
|
return c.txs[i]
|
|
}
|
|
record := &txRecord{tx: tx}
|
|
|
|
// If this new transaction record cannot be appended to the end of the
|
|
// txs slice (which would disobey ordering transactions by their block
|
|
// index), reslice and update the block's map of block indexes to txs
|
|
// slice indexes.
|
|
if len(c.txs) > 0 && c.txs[len(c.txs)-1].Tx().Index() > tx.Index() {
|
|
i := uint32(len(c.txs))
|
|
for i != 0 && c.txs[i-1].Tx().Index() >= tx.Index() {
|
|
i--
|
|
}
|
|
detached := c.txs[i:]
|
|
c.txs = append(c.txs[:i], record)
|
|
c.txIndexes[tx.Index()] = i
|
|
for i, r := range detached {
|
|
newIndex := uint32(i + len(c.txs))
|
|
c.txIndexes[r.Tx().Index()] = newIndex
|
|
}
|
|
c.txs = append(c.txs, detached...)
|
|
} else {
|
|
c.txIndexes[tx.Index()] = uint32(len(c.txs))
|
|
c.txs = append(c.txs, record)
|
|
}
|
|
return record
|
|
}
|
|
|
|
func (s *Store) blockTxRecordForInserts(tx *btcutil.Tx, block *Block) *txRecord {
|
|
return s.blockCollectionForInserts(block).txRecordForInserts(tx)
|
|
}
|
|
|
|
func (u *unconfirmedStore) txRecordForInserts(tx *btcutil.Tx) *txRecord {
|
|
r, ok := u.txs[*tx.Sha()]
|
|
if !ok {
|
|
r = &txRecord{tx: tx}
|
|
u.txs[*tx.Sha()] = r
|
|
for _, input := range r.Tx().MsgTx().TxIn {
|
|
u.previousOutpoints[input.PreviousOutpoint] = r
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
|
|
func (s *Store) moveMinedTx(r *txRecord, block *Block) error {
|
|
delete(s.unconfirmed.txs, *r.Tx().Sha())
|
|
|
|
// Find collection and insert records. Error out if there are records
|
|
// saved for this block and index.
|
|
key := BlockTxKey{r.Tx().Index(), block.Height}
|
|
b := s.blockCollectionForInserts(block)
|
|
txIndex := uint32(len(b.txs))
|
|
b.txIndexes[key.BlockIndex] = txIndex
|
|
b.txs = append(b.txs, r)
|
|
|
|
for _, input := range r.Tx().MsgTx().TxIn {
|
|
delete(s.unconfirmed.previousOutpoints, input.PreviousOutpoint)
|
|
|
|
// For all mined transactions with credits spent by this
|
|
// transaction, remove them from the spentBlockOutPoints map
|
|
// (signifying that there is no longer an unconfirmed
|
|
// transaction which spending that credit), and update the
|
|
// credit's spent by tracking with the block key of the
|
|
// newly-mined transaction.
|
|
prev, ok := s.unconfirmed.spentBlockOutPointKeys[input.PreviousOutpoint]
|
|
if !ok {
|
|
continue
|
|
}
|
|
delete(s.unconfirmed.spentBlockOutPointKeys, input.PreviousOutpoint)
|
|
delete(s.unconfirmed.spentBlockOutPoints, prev)
|
|
rr, err := s.lookupBlockTx(prev.BlockTxKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rr.credits[prev.OutputIndex].spentBy = &key
|
|
// debits should already be non-nil
|
|
r.debits.spends = append(r.debits.spends, prev)
|
|
}
|
|
|
|
// For each credit in r, if the credit is spent by another unconfirmed
|
|
// transaction, move the spending transaction from spentUnconfirmed
|
|
// (which signifies a transaction spending another unconfirmed tx) to
|
|
// spentBlockTxs (which signifies an unconfirmed transaction spending a
|
|
// confirmed tx) and modify the mined transaction's record to refer to
|
|
// the credit being spent by an unconfirmed transaction.
|
|
//
|
|
// If the credit is not spent, modify the store's unspent bookkeeping
|
|
// maps to include the credit and increment the amount deltas by the
|
|
// credit's value.
|
|
op := btcwire.OutPoint{Hash: *r.Tx().Sha()}
|
|
for i, credit := range r.credits {
|
|
if credit == nil {
|
|
continue
|
|
}
|
|
op.Index = uint32(i)
|
|
outputKey := BlockOutputKey{key, op.Index}
|
|
if rr, ok := s.unconfirmed.spentUnconfirmed[op]; ok {
|
|
delete(s.unconfirmed.spentUnconfirmed, op)
|
|
s.unconfirmed.spentBlockOutPointKeys[op] = outputKey
|
|
s.unconfirmed.spentBlockOutPoints[outputKey] = rr
|
|
credit.spentBy = &BlockTxKey{BlockHeight: -1}
|
|
} else if credit.spentBy == nil {
|
|
// Mark outpoint unspent.
|
|
s.unspent[op] = key
|
|
|
|
// Increment spendable amount delta as a result of
|
|
// moving this credit to this block.
|
|
value := r.Tx().MsgTx().TxOut[i].Value
|
|
b.amountDeltas.Spendable += btcutil.Amount(value)
|
|
}
|
|
}
|
|
|
|
// If this moved transaction debits from any previous credits, decrement
|
|
// the amount deltas by the total input amount. Because this
|
|
// transaction was moved from the unconfirmed transaction set, this can
|
|
// never be a coinbase.
|
|
if r.debits != nil {
|
|
b.amountDeltas.Spendable -= r.debits.amount
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// InsertTx records a transaction as belonging to a wallet's transaction
|
|
// history. If block is nil, the transaction is considered unspent, and the
|
|
// transaction's index must be unset. Otherwise, the transaction index must be
|
|
// set if a non-nil block is set.
|
|
//
|
|
// The transaction record is returned. Credits and debits may be added to the
|
|
// transaction by calling methods on the TxRecord.
|
|
func (s *Store) InsertTx(tx *btcutil.Tx, block *Block) (*TxRecord, error) {
|
|
// The receive time will be the earlier of now and the block time
|
|
// (if any).
|
|
received := time.Now()
|
|
|
|
// Verify that the index of the transaction within the block is
|
|
// set if a block is set, and unset if there is no block.
|
|
index := tx.Index()
|
|
switch {
|
|
case index == btcutil.TxIndexUnknown && block != nil:
|
|
return nil, errors.New("transaction block index unset")
|
|
case index != btcutil.TxIndexUnknown && block == nil:
|
|
return nil, errors.New("transaction block index set")
|
|
}
|
|
|
|
// Simply create or return the transaction record if this transaction
|
|
// is unconfirmed.
|
|
if block == nil {
|
|
r := s.unconfirmed.txRecordForInserts(tx)
|
|
r.received = received
|
|
return &TxRecord{BlockTxKey{BlockHeight: -1}, r, s}, nil
|
|
}
|
|
|
|
// Check if block records already exist for this tx. If so,
|
|
// we're done.
|
|
key := BlockTxKey{index, block.Height}
|
|
record, err := s.lookupBlockTx(key)
|
|
switch err.(type) {
|
|
case MissingValueError:
|
|
// handle it later
|
|
case nil:
|
|
// Verify that the txs actually match.
|
|
if *record.tx.Sha() != *tx.Sha() {
|
|
return nil, ErrInconsistentStore
|
|
}
|
|
|
|
return &TxRecord{key, record, s}, nil
|
|
}
|
|
|
|
// If the exact tx (not a double spend) is already included but
|
|
// unconfirmed, move it to a block.
|
|
if r, ok := s.unconfirmed.txs[*tx.Sha()]; ok {
|
|
r.Tx().SetIndex(tx.Index())
|
|
if err := s.moveMinedTx(r, block); err != nil {
|
|
return nil, err
|
|
}
|
|
return &TxRecord{key, r, s}, nil
|
|
}
|
|
|
|
// If this transaction is not already saved unconfirmed, remove all
|
|
// unconfirmed transactions that are now invalidated due to being a
|
|
// double spend. This also handles killing unconfirmed transaction
|
|
// spend chains if any other unconfirmed transactions spend outputs
|
|
// of the removed double spend.
|
|
if err := s.removeDoubleSpends(tx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r := s.blockTxRecordForInserts(tx, block)
|
|
if r.received.IsZero() {
|
|
if !block.Time.IsZero() && block.Time.Before(received) {
|
|
received = block.Time
|
|
}
|
|
r.received = received
|
|
}
|
|
return &TxRecord{key, r, s}, nil
|
|
}
|
|
|
|
// Received returns the earliest known time the transaction was received by.
|
|
func (t *TxRecord) Received() time.Time {
|
|
return t.received
|
|
}
|
|
|
|
// Block returns the block details for a transaction. If the transaction is
|
|
// unmined, both the block and returned error are nil.
|
|
func (t *TxRecord) Block() (*Block, error) {
|
|
coll, err := t.s.lookupBlock(t.BlockHeight)
|
|
if err != nil {
|
|
if err == MissingBlockError(-1) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return &coll.Block, nil
|
|
}
|
|
|
|
// AddDebits marks a transaction record as having debited from all them transaction
|
|
// credits in the spent slice. If spent is nil, the previous debits will be found,
|
|
// however this is an expensive lookup and should be avoided if possible.
|
|
func (t *TxRecord) AddDebits(spent []Credit) (Debits, error) {
|
|
if t.debits == nil {
|
|
// Find now-spent credits if no debits have been previously set
|
|
// and none were passed in by the caller.
|
|
if len(spent) == 0 {
|
|
foundSpent, err := t.s.findPreviousCredits(t.Tx())
|
|
if err != nil {
|
|
return Debits{}, err
|
|
}
|
|
spent = foundSpent
|
|
}
|
|
|
|
debitAmount, err := t.s.markOutputsSpent(spent, t)
|
|
if err != nil {
|
|
return Debits{}, err
|
|
}
|
|
t.debits = &debits{amount: debitAmount}
|
|
}
|
|
|
|
switch t.BlockHeight {
|
|
case -1: // unconfimred
|
|
for _, c := range spent {
|
|
op := c.OutPoint()
|
|
switch c.BlockHeight {
|
|
case -1: // unconfirmed
|
|
t.s.unconfirmed.spentUnconfirmed[*op] = t.txRecord
|
|
default:
|
|
key := c.outputKey()
|
|
t.s.unconfirmed.spentBlockOutPointKeys[*op] = key
|
|
t.s.unconfirmed.spentBlockOutPoints[key] = t.txRecord
|
|
}
|
|
}
|
|
|
|
default:
|
|
if t.debits.spends == nil {
|
|
prevOutputKeys := make([]BlockOutputKey, len(spent))
|
|
for i, c := range spent {
|
|
prevOutputKeys[i] = c.outputKey()
|
|
}
|
|
t.txRecord.debits.spends = prevOutputKeys
|
|
}
|
|
}
|
|
return Debits{t}, nil
|
|
}
|
|
|
|
// findPreviousCredits searches for all unspent credits that make up the inputs
|
|
// for tx.
|
|
func (s *Store) findPreviousCredits(tx *btcutil.Tx) ([]Credit, error) {
|
|
type createdCredit struct {
|
|
credit Credit
|
|
err error
|
|
}
|
|
|
|
inputs := tx.MsgTx().TxIn
|
|
creditChans := make([]chan createdCredit, len(inputs))
|
|
for i, txIn := range inputs {
|
|
creditChans[i] = make(chan createdCredit)
|
|
go func(i int, op btcwire.OutPoint) {
|
|
key, ok := s.unspent[op]
|
|
if !ok {
|
|
close(creditChans[i])
|
|
return
|
|
}
|
|
r, err := s.lookupBlockTx(key)
|
|
if err != nil {
|
|
creditChans[i] <- createdCredit{err: err}
|
|
return
|
|
}
|
|
t := &TxRecord{key, r, s}
|
|
c := Credit{t, op.Index}
|
|
creditChans[i] <- createdCredit{credit: c}
|
|
}(i, txIn.PreviousOutpoint)
|
|
}
|
|
spent := make([]Credit, 0, len(inputs))
|
|
for _, c := range creditChans {
|
|
cc, ok := <-c
|
|
if !ok {
|
|
continue
|
|
}
|
|
if cc.err != nil {
|
|
return nil, cc.err
|
|
}
|
|
spent = append(spent, cc.credit)
|
|
}
|
|
return spent, nil
|
|
}
|
|
|
|
// markOutputsSpent marks each previous credit spent by t as spent. The total
|
|
// input of all spent previous outputs is returned.
|
|
func (s *Store) markOutputsSpent(spent []Credit, t *TxRecord) (btcutil.Amount, error) {
|
|
var a btcutil.Amount
|
|
for _, prev := range spent {
|
|
op := prev.OutPoint()
|
|
switch prev.BlockHeight {
|
|
case -1: // unconfirmed
|
|
s.unconfirmed.spentUnconfirmed[*op] = t.txRecord
|
|
|
|
default:
|
|
// Update spent info.
|
|
credit := prev.txRecord.credits[prev.OutputIndex]
|
|
if credit.spentBy != nil {
|
|
if *credit.spentBy == t.BlockTxKey {
|
|
continue
|
|
}
|
|
return 0, ErrInconsistentStore
|
|
}
|
|
credit.spentBy = &t.BlockTxKey
|
|
delete(s.unspent, *op)
|
|
if t.BlockHeight == -1 { // unconfirmed
|
|
key := prev.outputKey()
|
|
s.unconfirmed.spentBlockOutPointKeys[*op] = key
|
|
s.unconfirmed.spentBlockOutPoints[key] = t.txRecord
|
|
}
|
|
|
|
// Increment total debited amount.
|
|
a += prev.Amount()
|
|
}
|
|
}
|
|
|
|
// If t refers to a mined transaction, update its block's amount deltas
|
|
// by the total debited amount.
|
|
if t.BlockHeight != -1 {
|
|
b, err := s.lookupBlock(t.BlockHeight)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
b.amountDeltas.Spendable -= a
|
|
}
|
|
|
|
return a, nil
|
|
}
|
|
|
|
func (r *txRecord) setCredit(index uint32, change bool, tx *btcutil.Tx) error {
|
|
if r.credits == nil {
|
|
r.credits = make([]*credit, 0, len(tx.MsgTx().TxOut))
|
|
}
|
|
for i := uint32(len(r.credits)); i <= index; i++ {
|
|
r.credits = append(r.credits, nil)
|
|
}
|
|
if r.credits[index] != nil {
|
|
if *r.tx.Sha() == *tx.Sha() {
|
|
return ErrDuplicateInsert
|
|
}
|
|
return ErrInconsistentStore
|
|
}
|
|
r.credits[index] = &credit{change: change}
|
|
return nil
|
|
}
|
|
|
|
// AddCredit marks the transaction record as containing a transaction output
|
|
// spendable by wallet. The output is added unspent, and is marked spent
|
|
// when a new transaction spending the output is inserted into the store.
|
|
func (t *TxRecord) AddCredit(index uint32, change bool) (Credit, error) {
|
|
if len(t.tx.MsgTx().TxOut) <= int(index) {
|
|
return Credit{}, errors.New("transaction output does not exist")
|
|
}
|
|
|
|
if err := t.txRecord.setCredit(index, change, t.tx); err != nil {
|
|
if err == ErrDuplicateInsert {
|
|
return Credit{t, index}, nil
|
|
}
|
|
return Credit{}, err
|
|
}
|
|
|
|
switch t.BlockHeight {
|
|
case -1: // unconfirmed
|
|
default:
|
|
b, err := t.s.lookupBlock(t.BlockHeight)
|
|
if err != nil {
|
|
return Credit{}, err
|
|
}
|
|
|
|
// New outputs are added unspent.
|
|
op := btcwire.OutPoint{Hash: *t.tx.Sha(), Index: index}
|
|
t.s.unspent[op] = t.BlockTxKey
|
|
switch a := t.tx.MsgTx().TxOut[index].Value; t.tx.Index() {
|
|
case 0: // Coinbase
|
|
b.amountDeltas.Reward += btcutil.Amount(a)
|
|
default:
|
|
b.amountDeltas.Spendable += btcutil.Amount(a)
|
|
}
|
|
}
|
|
|
|
return Credit{t, index}, nil
|
|
}
|
|
|
|
// Rollback removes all blocks at height onwards, moving any transactions within
|
|
// each block to the unconfirmed pool.
|
|
func (s *Store) Rollback(height int32) error {
|
|
i := len(s.blocks)
|
|
for i != 0 && s.blocks[i-1].Height >= height {
|
|
i--
|
|
}
|
|
detached := s.blocks[i:]
|
|
s.blocks = s.blocks[:i]
|
|
for _, b := range detached {
|
|
delete(s.blockIndexes, b.Block.Height)
|
|
for _, r := range b.txs {
|
|
oldTxIndex := r.Tx().Index()
|
|
|
|
// If the removed transaction is a coinbase, do not move
|
|
// it to unconfirmed.
|
|
if oldTxIndex == 0 {
|
|
continue
|
|
}
|
|
|
|
r.Tx().SetIndex(btcutil.TxIndexUnknown)
|
|
s.unconfirmed.txs[*r.Tx().Sha()] = r
|
|
for _, input := range r.Tx().MsgTx().TxIn {
|
|
op := input.PreviousOutpoint
|
|
s.unconfirmed.previousOutpoints[op] = r
|
|
}
|
|
|
|
// For each detached spent credit, remove from the
|
|
// store's unspent map, and lookup the spender and
|
|
// modify its debit record to reference spending an
|
|
// unconfirmed transaction.
|
|
for outIdx, credit := range r.credits {
|
|
if credit == nil {
|
|
continue
|
|
}
|
|
|
|
op := btcwire.OutPoint{
|
|
Hash: *r.Tx().Sha(),
|
|
Index: uint32(outIdx),
|
|
}
|
|
delete(s.unspent, op)
|
|
|
|
spenderKey := credit.spentBy
|
|
if spenderKey == nil {
|
|
continue
|
|
}
|
|
|
|
prev := BlockOutputKey{
|
|
BlockTxKey: BlockTxKey{
|
|
BlockIndex: oldTxIndex,
|
|
BlockHeight: b.Height,
|
|
},
|
|
OutputIndex: uint32(outIdx),
|
|
}
|
|
|
|
// Lookup txRecord of the spending transaction. Spent
|
|
// tracking differs slightly depending on whether the
|
|
// spender is confirmed or not.
|
|
switch spenderKey.BlockHeight {
|
|
case -1: // unconfirmed
|
|
spender, ok := s.unconfirmed.spentBlockOutPoints[prev]
|
|
if !ok {
|
|
return ErrInconsistentStore
|
|
}
|
|
|
|
// Swap the maps the spender is saved in.
|
|
delete(s.unconfirmed.spentBlockOutPointKeys, op)
|
|
delete(s.unconfirmed.spentBlockOutPoints, prev)
|
|
s.unconfirmed.spentUnconfirmed[op] = spender
|
|
|
|
default:
|
|
spender, err := s.lookupBlockTx(*spenderKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if spender.debits == nil {
|
|
return MissingDebitsError(*spenderKey)
|
|
}
|
|
|
|
current := BlockOutputKey{
|
|
BlockTxKey: BlockTxKey{BlockHeight: -1},
|
|
}
|
|
err = spender.swapDebits(prev, current)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// If this transaction debits any previous credits,
|
|
// modify each previous credit to mark it as spent
|
|
// by an unconfirmed tx.
|
|
if r.debits != nil {
|
|
for _, prev := range r.debits.spends {
|
|
rr, err := s.lookupBlockTx(prev.BlockTxKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c, err := rr.lookupBlockCredit(prev)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
op := btcwire.OutPoint{
|
|
Hash: *rr.Tx().Sha(),
|
|
Index: prev.OutputIndex,
|
|
}
|
|
s.unconfirmed.spentBlockOutPointKeys[op] = prev
|
|
s.unconfirmed.spentBlockOutPoints[prev] = r
|
|
c.spentBy = &BlockTxKey{BlockHeight: -1}
|
|
}
|
|
|
|
// Debit tracking for unconfirmed transactions is
|
|
// done by the unconfirmed store's maps, and not
|
|
// in the txRecord itself.
|
|
r.debits.spends = nil
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *txRecord) swapDebits(previous, current BlockOutputKey) error {
|
|
for i, outputKey := range r.debits.spends {
|
|
if outputKey == previous {
|
|
r.debits.spends[i] = current
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return MissingCreditError(previous)
|
|
}
|
|
|
|
// UnminedDebitTxs returns the underlying transactions for all wallet
|
|
// transactions which debit from previous outputs and are not known to have
|
|
// been mined in a block.
|
|
func (s *Store) UnminedDebitTxs() []*btcutil.Tx {
|
|
unmined := make([]*btcutil.Tx, 0, len(s.unconfirmed.txs))
|
|
for _, r := range s.unconfirmed.spentBlockOutPoints {
|
|
unmined = append(unmined, r.Tx())
|
|
}
|
|
for _, r := range s.unconfirmed.spentUnconfirmed {
|
|
unmined = append(unmined, r.Tx())
|
|
}
|
|
return unmined
|
|
}
|
|
|
|
// removeDoubleSpends checks for any unconfirmed transactions which would
|
|
// introduce a double spend if tx was added to the store (either as a confirmed
|
|
// or unconfirmed transaction). If one is found, it and all transactions which
|
|
// spends its outputs (if any) are removed, and all previous inputs for any
|
|
// removed transactions are set to unspent.
|
|
func (s *Store) removeDoubleSpends(tx *btcutil.Tx) error {
|
|
if ds := s.unconfirmed.findDoubleSpend(tx); ds != nil {
|
|
return s.removeConflict(ds)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (u *unconfirmedStore) findDoubleSpend(tx *btcutil.Tx) *txRecord {
|
|
for _, input := range tx.MsgTx().TxIn {
|
|
if r, ok := u.previousOutpoints[input.PreviousOutpoint]; ok {
|
|
return r
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// removeConflict removes an unconfirmed transaction record and all spend chains
|
|
// deriving from it from the store. This is designed to remove transactions
|
|
// that would otherwise result in double spend conflicts if left in the store.
|
|
// All not-removed credits spent by removed transactions are set unspent.
|
|
func (s *Store) removeConflict(r *txRecord) error {
|
|
u := &s.unconfirmed
|
|
|
|
// If this transaction contains any spent credits (which must be spent by
|
|
// other unconfirmed transactions), recursively remove each spender.
|
|
for i, credit := range r.credits {
|
|
if credit == nil || credit.spentBy == nil {
|
|
continue
|
|
}
|
|
op := btcwire.NewOutPoint(r.Tx().Sha(), uint32(i))
|
|
nextSpender, ok := u.spentUnconfirmed[*op]
|
|
if !ok {
|
|
return ErrInconsistentStore
|
|
}
|
|
if err := s.removeConflict(nextSpender); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// If this tx spends any previous credits, set each unspent.
|
|
for _, input := range r.Tx().MsgTx().TxIn {
|
|
delete(u.previousOutpoints, input.PreviousOutpoint)
|
|
|
|
// For all mined transactions with credits spent by this
|
|
// conflicting transaction, remove from the bookkeeping maps
|
|
// and set each previous record's credit as unspent.
|
|
prevKey, ok := u.spentBlockOutPointKeys[input.PreviousOutpoint]
|
|
if ok {
|
|
delete(u.spentBlockOutPointKeys, input.PreviousOutpoint)
|
|
delete(u.spentBlockOutPoints, prevKey)
|
|
prev, err := s.lookupBlockTx(prevKey.BlockTxKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
prev.credits[prevKey.OutputIndex].spentBy = nil
|
|
continue
|
|
}
|
|
|
|
// For all unmined transactions with credits spent by this
|
|
// conflicting transaction, remove from the unspent store's
|
|
// spent tracking.
|
|
//
|
|
// Spend tracking is only handled by these maps, so there is
|
|
// no need to modify the record and unset a spent-by pointer.
|
|
if _, ok := u.spentUnconfirmed[input.PreviousOutpoint]; ok {
|
|
delete(u.spentUnconfirmed, input.PreviousOutpoint)
|
|
}
|
|
}
|
|
|
|
delete(u.txs, *r.Tx().Sha())
|
|
for _, input := range r.Tx().MsgTx().TxIn {
|
|
delete(u.previousOutpoints, input.PreviousOutpoint)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UnspentOutputs returns all unspent received transaction outputs.
|
|
// The order is undefined.
|
|
func (s *Store) UnspentOutputs() ([]Credit, error) {
|
|
type createdCredit struct {
|
|
credit Credit
|
|
err error
|
|
}
|
|
|
|
creditChans := make([]chan createdCredit, len(s.unspent))
|
|
i := 0
|
|
for op, key := range s.unspent {
|
|
creditChans[i] = make(chan createdCredit)
|
|
go func(i int, key BlockTxKey, opIndex uint32) {
|
|
r, err := s.lookupBlockTx(key)
|
|
if err != nil {
|
|
creditChans[i] <- createdCredit{err: err}
|
|
return
|
|
}
|
|
t := &TxRecord{key, r, s}
|
|
c := Credit{t, opIndex}
|
|
creditChans[i] <- createdCredit{credit: c}
|
|
}(i, key, op.Index)
|
|
i++
|
|
}
|
|
|
|
unspent := make([]Credit, len(s.unspent))
|
|
for i, c := range creditChans {
|
|
cc := <-c
|
|
if cc.err != nil {
|
|
return nil, cc.err
|
|
}
|
|
unspent[i] = cc.credit
|
|
}
|
|
|
|
for _, r := range s.unconfirmed.txs {
|
|
for outputIndex, credit := range r.credits {
|
|
if credit == nil || credit.spentBy != nil {
|
|
continue
|
|
}
|
|
key := BlockTxKey{BlockHeight: -1}
|
|
txRecord := &TxRecord{key, r, s}
|
|
c := Credit{txRecord, uint32(outputIndex)}
|
|
unspent = append(unspent, c)
|
|
}
|
|
}
|
|
|
|
return unspent, nil
|
|
}
|
|
|
|
// unspentTx is a type defined here so it can be sorted with the sort
|
|
// package. It is used to provide a sorted range over all transactions
|
|
// in a block with unspent outputs.
|
|
type unspentTx struct {
|
|
blockIndex int
|
|
sliceIndex uint32
|
|
}
|
|
|
|
type creditSlice []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].Tx().Sha() == s[j].Tx().Sha():
|
|
return s[i].OutputIndex < s[j].OutputIndex
|
|
|
|
// If both transactions are unmined, sort by their received date.
|
|
case s[i].BlockIndex == -1 && s[j].BlockIndex == -1:
|
|
return s[i].received.Before(s[j].received)
|
|
|
|
// Unmined (newer) txs always come last.
|
|
case s[i].BlockIndex == -1:
|
|
return false
|
|
case s[j].BlockIndex == -1:
|
|
return true
|
|
|
|
// If both txs are mined in different blocks, sort by block height.
|
|
case s[i].BlockHeight != s[j].BlockHeight:
|
|
return s[i].BlockHeight < s[j].BlockHeight
|
|
|
|
// Both txs share the same block, sort by block index.
|
|
default:
|
|
return s[i].BlockIndex < s[j].BlockIndex
|
|
}
|
|
}
|
|
|
|
func (s creditSlice) Swap(i, j int) {
|
|
s[i], s[j] = s[j], s[i]
|
|
}
|
|
|
|
// SortedUnspentOutputs returns all unspent recevied transaction outputs.
|
|
// The order is first unmined transactions (sorted by receive date), then
|
|
// mined transactions in increasing number of confirmations. Transactions
|
|
// in the same block (same number of confirmations) are sorted by block
|
|
// index in increasing order. Credits (outputs) from the same transaction
|
|
// are sorted by output index in increasing order.
|
|
func (s *Store) SortedUnspentOutputs() ([]Credit, error) {
|
|
unspent, err := s.UnspentOutputs()
|
|
if err != nil {
|
|
return []Credit{}, err
|
|
}
|
|
sort.Sort(sort.Reverse(creditSlice(unspent)))
|
|
return unspent, nil
|
|
}
|
|
|
|
// confirmed checks whether a transaction at height txHeight has met
|
|
// minconf confirmations for a blockchain at height curHeight.
|
|
func confirmed(minconf int, txHeight, curHeight int32) bool {
|
|
return confirms(txHeight, curHeight) >= int32(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
|
|
}
|
|
}
|
|
|
|
// Balance returns the spendable wallet balance (total value of all unspent
|
|
// transaction outputs) given a minimum of minConf confirmations, calculated
|
|
// at a current chain height of curHeight. Coinbase outputs are only included
|
|
// in the balance if maturity has been reached.
|
|
func (s *Store) Balance(minConf int, chainHeight int32) (btcutil.Amount, error) {
|
|
var bal btcutil.Amount
|
|
|
|
// Shadow these functions to avoid repeating arguments unnecesarily.
|
|
confirms := func(height int32) int32 {
|
|
return confirms(height, chainHeight)
|
|
}
|
|
confirmed := func(height int32) bool {
|
|
return confirmed(minConf, height, chainHeight)
|
|
}
|
|
|
|
for _, b := range s.blocks {
|
|
if confirmed(b.Height) {
|
|
bal += b.amountDeltas.Spendable
|
|
if confirms(b.Height) >= btcchain.CoinbaseMaturity {
|
|
bal += b.amountDeltas.Reward
|
|
}
|
|
continue
|
|
}
|
|
// If there are still blocks that contain debiting transactions,
|
|
// decrement the balance if they spend credits meeting minConf
|
|
// confirmations.
|
|
for _, r := range b.txs {
|
|
if r.debits == nil {
|
|
continue
|
|
}
|
|
for _, prev := range r.debits.spends {
|
|
if !confirmed(prev.BlockHeight) {
|
|
continue
|
|
}
|
|
r, err := s.lookupBlockTx(prev.BlockTxKey)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
v := r.Tx().MsgTx().TxOut[prev.OutputIndex].Value
|
|
bal -= btcutil.Amount(v)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Unconfirmed transactions which spend previous credits debit from
|
|
// the returned balance, even with minConf > 0.
|
|
for prev := range s.unconfirmed.spentBlockOutPoints {
|
|
if confirmed(prev.BlockHeight) {
|
|
r, err := s.lookupBlockTx(prev.BlockTxKey)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
v := r.Tx().MsgTx().TxOut[prev.OutputIndex].Value
|
|
bal -= btcutil.Amount(v)
|
|
}
|
|
}
|
|
|
|
// If unconfirmed credits are included, tally them as well.
|
|
if minConf == 0 {
|
|
for _, r := range s.unconfirmed.txs {
|
|
for i, c := range r.credits {
|
|
if c == nil {
|
|
continue
|
|
}
|
|
if c.spentBy == nil {
|
|
v := r.Tx().MsgTx().TxOut[i].Value
|
|
bal += btcutil.Amount(v)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return bal, nil
|
|
}
|
|
|
|
// Records returns a chronologically-ordered slice of all transaction records
|
|
// saved by the store. This is sorted first by block height in increasing
|
|
// order, and then by transaction index for each tx in a block.
|
|
func (s *Store) Records() (records []*TxRecord) {
|
|
for _, b := range s.blocks {
|
|
for _, r := range b.txs {
|
|
key := BlockTxKey{r.tx.Index(), b.Block.Height}
|
|
records = append(records, &TxRecord{key, r, s})
|
|
}
|
|
}
|
|
|
|
// Unconfirmed records are saved unsorted, and must be sorted by
|
|
// received date on the fly.
|
|
unconfirmed := make([]*TxRecord, 0, len(s.unconfirmed.txs))
|
|
for _, r := range s.unconfirmed.txs {
|
|
key := BlockTxKey{BlockHeight: -1}
|
|
unconfirmed = append(unconfirmed, &TxRecord{key, r, s})
|
|
}
|
|
sort.Sort(byReceiveDate(unconfirmed))
|
|
records = append(records, unconfirmed...)
|
|
|
|
return
|
|
}
|
|
|
|
// Implementation of sort.Interface to sort transaction records by their
|
|
// receive date.
|
|
type byReceiveDate []*TxRecord
|
|
|
|
func (r byReceiveDate) Len() int { return len(r) }
|
|
func (r byReceiveDate) Less(i, j int) bool { return r[i].received.Before(r[j].received) }
|
|
func (r byReceiveDate) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
|
|
|
// Debits returns the debit record for the transaction, or a non-nil error if
|
|
// the transaction does not debit from any previous transaction credits.
|
|
func (t *TxRecord) Debits() (Debits, error) {
|
|
if t.debits == nil {
|
|
return Debits{}, errors.New("no debits")
|
|
}
|
|
return Debits{t}, nil
|
|
}
|
|
|
|
// Credits returns all credit records for this transaction's outputs that are or
|
|
// were spendable by wallet.
|
|
func (t *TxRecord) Credits() []Credit {
|
|
credits := make([]Credit, 0, len(t.credits))
|
|
for i, c := range t.credits {
|
|
if c != nil {
|
|
credits = append(credits, Credit{t, uint32(i)})
|
|
}
|
|
}
|
|
return credits
|
|
}
|
|
|
|
// HasCredit returns whether the transaction output at the passed index is
|
|
// a wallet credit.
|
|
func (t *TxRecord) HasCredit(i int) bool {
|
|
if len(t.credits) <= i {
|
|
return false
|
|
}
|
|
return t.credits[i] != nil
|
|
}
|
|
|
|
// InputAmount returns the total amount debited from previous credits.
|
|
func (d Debits) InputAmount() btcutil.Amount {
|
|
return d.txRecord.debits.amount
|
|
}
|
|
|
|
// OutputAmount returns the total amount of all outputs for a transaction.
|
|
func (t *TxRecord) OutputAmount(ignoreChange bool) btcutil.Amount {
|
|
a := btcutil.Amount(0)
|
|
for i, txOut := range t.Tx().MsgTx().TxOut {
|
|
if ignoreChange {
|
|
switch cs := t.credits; {
|
|
case i < len(cs) && cs[i] != nil && cs[i].change:
|
|
continue
|
|
}
|
|
}
|
|
a += btcutil.Amount(txOut.Value)
|
|
}
|
|
return a
|
|
}
|
|
|
|
// Fee returns the difference between the debited amount and the total
|
|
// transaction output.
|
|
func (d Debits) Fee() btcutil.Amount {
|
|
return d.InputAmount() - d.OutputAmount(false)
|
|
}
|
|
|
|
// Addresses parses the pubkey script, extracting all addresses for a
|
|
// standard script.
|
|
func (c Credit) Addresses(net *btcnet.Params) (btcscript.ScriptClass,
|
|
[]btcutil.Address, int, error) {
|
|
|
|
msgTx := c.Tx().MsgTx()
|
|
pkScript := msgTx.TxOut[c.OutputIndex].PkScript
|
|
return btcscript.ExtractPkScriptAddrs(pkScript, net)
|
|
}
|
|
|
|
// Change returns whether the credit is the result of a change output.
|
|
func (c Credit) Change() bool {
|
|
return c.txRecord.credits[c.OutputIndex].change
|
|
}
|
|
|
|
// Confirmed returns whether a transaction has reached some target number of
|
|
// confirmations, given the current best chain height.
|
|
func (t *TxRecord) Confirmed(target int, chainHeight int32) bool {
|
|
return confirmed(target, t.BlockHeight, chainHeight)
|
|
}
|
|
|
|
// Confirmations returns the total number of confirmations a transaction has
|
|
// reached, given the current best chain height.
|
|
func (t *TxRecord) Confirmations(chainHeight int32) int32 {
|
|
return confirms(t.BlockHeight, chainHeight)
|
|
}
|
|
|
|
// IsCoinbase returns whether the transaction is a coinbase.
|
|
func (t *TxRecord) IsCoinbase() bool {
|
|
return t.BlockHeight != -1 && t.BlockIndex == 0
|
|
}
|
|
|
|
// Amount returns the amount credited to the account from a transaction output.
|
|
func (c Credit) Amount() btcutil.Amount {
|
|
msgTx := c.Tx().MsgTx()
|
|
return btcutil.Amount(msgTx.TxOut[c.OutputIndex].Value)
|
|
}
|
|
|
|
// OutPoint returns the outpoint needed to include in a transaction input
|
|
// to spend this output.
|
|
func (c Credit) OutPoint() *btcwire.OutPoint {
|
|
return btcwire.NewOutPoint(c.Tx().Sha(), c.OutputIndex)
|
|
}
|
|
|
|
// outputKey creates and returns the block lookup key for this credit.
|
|
func (c Credit) outputKey() BlockOutputKey {
|
|
return BlockOutputKey{
|
|
BlockTxKey: c.BlockTxKey,
|
|
OutputIndex: c.OutputIndex,
|
|
}
|
|
}
|
|
|
|
// Spent returns whether the transaction output is currently spent or not.
|
|
func (c Credit) Spent() bool {
|
|
return c.txRecord.credits[c.OutputIndex].spentBy != nil
|
|
}
|
|
|
|
// TxOut returns the transaction output which this credit references.
|
|
func (c Credit) TxOut() *btcwire.TxOut {
|
|
return c.Tx().MsgTx().TxOut[c.OutputIndex]
|
|
}
|
|
|
|
// Tx returns the underlying transaction.
|
|
func (r *txRecord) Tx() *btcutil.Tx {
|
|
return r.tx
|
|
}
|