decred.org/dcrwallet/v3@v3.1.0/wallet/utxos.go (about)

     1  // Copyright (c) 2016-2021 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package wallet
     6  
     7  import (
     8  	"context"
     9  	"time"
    10  
    11  	"decred.org/dcrwallet/v3/errors"
    12  	"decred.org/dcrwallet/v3/internal/compat"
    13  	"decred.org/dcrwallet/v3/wallet/txauthor"
    14  	"decred.org/dcrwallet/v3/wallet/udb"
    15  	"decred.org/dcrwallet/v3/wallet/walletdb"
    16  	"github.com/decred/dcrd/dcrutil/v4"
    17  	"github.com/decred/dcrd/txscript/v4/stdscript"
    18  	"github.com/decred/dcrd/wire"
    19  )
    20  
    21  // OutputSelectionPolicy describes the rules for selecting an output from the
    22  // wallet.
    23  type OutputSelectionPolicy struct {
    24  	Account               uint32
    25  	RequiredConfirmations int32
    26  }
    27  
    28  func (p *OutputSelectionPolicy) meetsRequiredConfs(txHeight, curHeight int32) bool {
    29  	return confirmed(p.RequiredConfirmations, txHeight, curHeight)
    30  }
    31  
    32  // UnspentOutputs fetches all unspent outputs from the wallet that match rules
    33  // described in the passed policy.
    34  func (w *Wallet) UnspentOutputs(ctx context.Context, policy OutputSelectionPolicy) ([]*TransactionOutput, error) {
    35  	const op errors.Op = "wallet.UnspentOutputs"
    36  
    37  	defer w.lockedOutpointMu.Unlock()
    38  	w.lockedOutpointMu.Lock()
    39  
    40  	var outputResults []*TransactionOutput
    41  	err := walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error {
    42  		addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey)
    43  
    44  		_, tipHeight := w.txStore.MainChainTip(dbtx)
    45  
    46  		// TODO: actually stream outputs from the db instead of fetching
    47  		// all of them at once.
    48  		outputs, err := w.txStore.UnspentOutputs(dbtx)
    49  		if err != nil {
    50  			return err
    51  		}
    52  
    53  		for _, output := range outputs {
    54  			// Ignore outputs that haven't reached the required
    55  			// number of confirmations.
    56  			if !policy.meetsRequiredConfs(output.Height, tipHeight) {
    57  				continue
    58  			}
    59  
    60  			// Ignore outputs that are not controlled by the account.
    61  			_, addrs := stdscript.ExtractAddrs(scriptVersionAssumed, output.PkScript, w.chainParams)
    62  			if len(addrs) == 0 {
    63  				// Cannot determine which account this belongs
    64  				// to without a valid address.  TODO: Fix this
    65  				// by saving outputs per account, or accounts
    66  				// per output.
    67  				continue
    68  			}
    69  			outputAcct, err := w.manager.AddrAccount(addrmgrNs, addrs[0])
    70  			if err != nil {
    71  				return err
    72  			}
    73  			if outputAcct != policy.Account {
    74  				continue
    75  			}
    76  
    77  			// Stakebase isn't exposed by wtxmgr so those will be
    78  			// OutputKindNormal for now.
    79  			outputSource := OutputKindNormal
    80  			if output.FromCoinBase {
    81  				outputSource = OutputKindCoinbase
    82  			}
    83  
    84  			result := &TransactionOutput{
    85  				OutPoint: output.OutPoint,
    86  				Output: wire.TxOut{
    87  					Value: int64(output.Amount),
    88  					// TODO: version is bogus but there is
    89  					// only version 0 at time of writing.
    90  					Version:  0,
    91  					PkScript: output.PkScript,
    92  				},
    93  				OutputKind:      outputSource,
    94  				ContainingBlock: BlockIdentity(output.Block),
    95  				ReceiveTime:     output.Received,
    96  			}
    97  			outputResults = append(outputResults, result)
    98  		}
    99  
   100  		return nil
   101  	})
   102  	if err != nil {
   103  		return nil, errors.E(op, err)
   104  	}
   105  	return outputResults, nil
   106  }
   107  
   108  // SelectInputs selects transaction inputs to redeem unspent outputs stored in
   109  // the wallet.  It returns an input detail summary.
   110  func (w *Wallet) SelectInputs(ctx context.Context, targetAmount dcrutil.Amount, policy OutputSelectionPolicy) (inputDetail *txauthor.InputDetail, err error) {
   111  	const op errors.Op = "wallet.SelectInputs"
   112  
   113  	defer w.lockedOutpointMu.Unlock()
   114  	w.lockedOutpointMu.Lock()
   115  
   116  	err = walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error {
   117  		addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey)
   118  		_, tipHeight := w.txStore.MainChainTip(dbtx)
   119  
   120  		if policy.Account != udb.ImportedAddrAccount {
   121  			lastAcct, err := w.manager.LastAccount(addrmgrNs)
   122  			if err != nil {
   123  				return err
   124  			}
   125  			if policy.Account > lastAcct {
   126  				return errors.E(errors.NotExist, "account not found")
   127  			}
   128  		}
   129  
   130  		sourceImpl := w.txStore.MakeInputSource(dbtx, policy.Account,
   131  			policy.RequiredConfirmations, tipHeight, nil)
   132  		var err error
   133  		inputDetail, err = sourceImpl.SelectInputs(targetAmount)
   134  		return err
   135  	})
   136  	if err != nil {
   137  		err = errors.E(op, err)
   138  	}
   139  	return inputDetail, err
   140  }
   141  
   142  // OutputInfo describes additional info about an output which can be queried
   143  // using an outpoint.
   144  type OutputInfo struct {
   145  	Received     time.Time
   146  	Amount       dcrutil.Amount
   147  	FromCoinbase bool
   148  }
   149  
   150  // OutputInfo queries the wallet for additional transaction output info
   151  // regarding an outpoint.
   152  func (w *Wallet) OutputInfo(ctx context.Context, out *wire.OutPoint) (OutputInfo, error) {
   153  	const op errors.Op = "wallet.OutputInfo"
   154  	var info OutputInfo
   155  	err := walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error {
   156  		txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey)
   157  
   158  		txDetails, err := w.txStore.TxDetails(txmgrNs, &out.Hash)
   159  		if err != nil {
   160  			return err
   161  		}
   162  		if out.Index >= uint32(len(txDetails.TxRecord.MsgTx.TxOut)) {
   163  			return errors.Errorf("transaction has no output %d", out.Index)
   164  		}
   165  
   166  		info.Received = txDetails.Received
   167  		info.Amount = dcrutil.Amount(txDetails.TxRecord.MsgTx.TxOut[out.Index].Value)
   168  		info.FromCoinbase = compat.IsEitherCoinBaseTx(&txDetails.TxRecord.MsgTx)
   169  		return nil
   170  	})
   171  	if err != nil {
   172  		return info, errors.E(op, err)
   173  	}
   174  	return info, nil
   175  }