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

     1  // Copyright (c) 2016-2020 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  
    10  	"decred.org/dcrwallet/v3/errors"
    11  	"decred.org/dcrwallet/v3/rpc/client/dcrd"
    12  	"decred.org/dcrwallet/v3/wallet/walletdb"
    13  	"github.com/decred/dcrd/chaincfg/chainhash"
    14  	dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v4"
    15  	"github.com/decred/dcrd/txscript/v4/stdaddr"
    16  	"golang.org/x/sync/errgroup"
    17  )
    18  
    19  // LiveTicketHashes returns the hashes of live tickets that the wallet has
    20  // purchased or has voting authority for.
    21  func (w *Wallet) LiveTicketHashes(ctx context.Context, rpcCaller Caller, includeImmature bool) ([]chainhash.Hash, error) {
    22  	const op errors.Op = "wallet.LiveTicketHashes"
    23  
    24  	var ticketHashes []chainhash.Hash
    25  	var maybeLive []*chainhash.Hash
    26  
    27  	extraTickets := w.stakeMgr.DumpSStxHashes()
    28  
    29  	var tipHeight int32 // Assigned in view below.
    30  
    31  	err := walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error {
    32  		txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey)
    33  
    34  		// Remove tickets from the extraTickets slice if they will appear in the
    35  		// ticket iteration below.
    36  		hashes := extraTickets
    37  		extraTickets = hashes[:0]
    38  		for i := range hashes {
    39  			h := &hashes[i]
    40  			if !w.txStore.ExistsTx(txmgrNs, h) {
    41  				extraTickets = append(extraTickets, *h)
    42  			}
    43  		}
    44  
    45  		_, tipHeight = w.txStore.MainChainTip(dbtx)
    46  
    47  		it := w.txStore.IterateTickets(dbtx)
    48  		defer it.Close()
    49  		for it.Next() {
    50  			// Tickets that are mined at a height beyond the expiry height can
    51  			// not be live.
    52  			if ticketExpired(w.chainParams, it.Block.Height, tipHeight) {
    53  				continue
    54  			}
    55  
    56  			// Tickets that have not reached ticket maturity are immature.
    57  			// Exclude them unless the caller requested to include immature
    58  			// tickets.
    59  			if !ticketMatured(w.chainParams, it.Block.Height, tipHeight) {
    60  				if includeImmature {
    61  					ticketHashes = append(ticketHashes, it.Hash)
    62  				}
    63  				continue
    64  			}
    65  
    66  			// The ticket may be live.  Because the selected state of tickets is
    67  			// not yet known by the wallet, this must be queried over RPC.  Add
    68  			// this hash to a slice of ticket purchase hashes to check later.
    69  			hash := it.Hash
    70  			maybeLive = append(maybeLive, &hash)
    71  		}
    72  		return it.Err()
    73  	})
    74  	if err != nil {
    75  		return nil, errors.E(op, err)
    76  	}
    77  
    78  	// Determine if the extra tickets are immature or possibly live.  Because
    79  	// these transactions are not part of the wallet's transaction history, dcrd
    80  	// must be queried for their blockchain height.  This functionality requires
    81  	// the dcrd transaction index to be enabled.
    82  	var g errgroup.Group
    83  	type extraTicketResult struct {
    84  		valid  bool // unspent with known height
    85  		height int32
    86  	}
    87  	extraTicketResults := make([]extraTicketResult, len(extraTickets))
    88  	for i := range extraTickets {
    89  		i := i
    90  		g.Go(func() error {
    91  			// gettxout is used first as an optimization to check that output 0
    92  			// of the ticket is unspent.
    93  			var txOut *dcrdtypes.GetTxOutResult
    94  			const index = 0
    95  			const tree = 1
    96  			err := rpcCaller.Call(ctx, "gettxout", &txOut, extraTickets[i].String(), index, tree)
    97  			if err != nil || txOut == nil {
    98  				return nil
    99  			}
   100  			var grt struct {
   101  				BlockHeight int32 `json:"blockheight"`
   102  			}
   103  			err = rpcCaller.Call(ctx, "getrawtransaction", &grt, extraTickets[i].String(), 1)
   104  			if err != nil {
   105  				return nil
   106  			}
   107  			extraTicketResults[i] = extraTicketResult{true, grt.BlockHeight}
   108  			return nil
   109  		})
   110  	}
   111  	err = g.Wait()
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  	for i := range extraTickets {
   116  		r := &extraTicketResults[i]
   117  		if !r.valid {
   118  			continue
   119  		}
   120  		// Same checks as above in the db view.
   121  		if ticketExpired(w.chainParams, r.height, tipHeight) {
   122  			continue
   123  		}
   124  		if !ticketMatured(w.chainParams, r.height, tipHeight) {
   125  			if includeImmature {
   126  				ticketHashes = append(ticketHashes, extraTickets[i])
   127  			}
   128  			continue
   129  		}
   130  		maybeLive = append(maybeLive, &extraTickets[i])
   131  	}
   132  
   133  	// If there are no possibly live tickets to check, ticketHashes contains all
   134  	// of the results.
   135  	if len(maybeLive) == 0 {
   136  		return ticketHashes, nil
   137  	}
   138  
   139  	// Use RPC to query which of the possibly-live tickets are really live.
   140  	rpc := dcrd.New(rpcCaller)
   141  	live, err := rpc.ExistsLiveTickets(ctx, maybeLive)
   142  	if err != nil {
   143  		return nil, errors.E(op, err)
   144  	}
   145  	for i, h := range maybeLive {
   146  		if live.Get(i) {
   147  			ticketHashes = append(ticketHashes, *h)
   148  		}
   149  	}
   150  
   151  	return ticketHashes, nil
   152  }
   153  
   154  // TicketHashesForVotingAddress returns the hashes of all tickets with voting
   155  // rights delegated to votingAddr.  This function does not return the hashes of
   156  // pruned tickets.
   157  func (w *Wallet) TicketHashesForVotingAddress(ctx context.Context, votingAddr stdaddr.Address) ([]chainhash.Hash, error) {
   158  	const op errors.Op = "wallet.TicketHashesForVotingAddress"
   159  
   160  	var ticketHashes []chainhash.Hash
   161  	err := walletdb.View(ctx, w.db, func(tx walletdb.ReadTx) error {
   162  		stakemgrNs := tx.ReadBucket(wstakemgrNamespaceKey)
   163  		txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey)
   164  
   165  		dump, err := w.stakeMgr.DumpSStxHashesForAddress(
   166  			stakemgrNs, votingAddr)
   167  		if err != nil {
   168  			return err
   169  		}
   170  
   171  		// Exclude hashes for unsaved transactions.
   172  		ticketHashes = dump[:0]
   173  		for i := range dump {
   174  			h := &dump[i]
   175  			if w.txStore.ExistsTx(txmgrNs, h) {
   176  				ticketHashes = append(ticketHashes, *h)
   177  			}
   178  		}
   179  
   180  		return nil
   181  	})
   182  	if err != nil {
   183  		return nil, errors.E(op, err)
   184  	}
   185  	return ticketHashes, nil
   186  }