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 }