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 }