gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/wallet/transactions.go (about) 1 package wallet 2 3 import ( 4 "encoding/binary" 5 "errors" 6 "fmt" 7 "sort" 8 9 "gitlab.com/SiaPrime/SiaPrime/build" 10 "gitlab.com/SiaPrime/SiaPrime/encoding" 11 "gitlab.com/SiaPrime/SiaPrime/modules" 12 "gitlab.com/SiaPrime/SiaPrime/types" 13 ) 14 15 var ( 16 errOutOfBounds = errors.New("requesting transactions at unknown confirmation heights") 17 ) 18 19 // AddressTransactions returns all of the wallet transactions associated with a 20 // single unlock hash. 21 func (w *Wallet) AddressTransactions(uh types.UnlockHash) (pts []modules.ProcessedTransaction, err error) { 22 if err := w.tg.Add(); err != nil { 23 return []modules.ProcessedTransaction{}, err 24 } 25 defer w.tg.Done() 26 // ensure durability of reported transactions 27 w.mu.Lock() 28 defer w.mu.Unlock() 29 if err = w.syncDB(); err != nil { 30 return 31 } 32 33 txnIndices, _ := dbGetAddrTransactions(w.dbTx, uh) 34 for _, i := range txnIndices { 35 pt, err := dbGetProcessedTransaction(w.dbTx, i) 36 if err != nil { 37 continue 38 } 39 pts = append(pts, pt) 40 } 41 return pts, nil 42 } 43 44 // AddressUnconfirmedTransactions returns all of the unconfirmed wallet transactions 45 // related to a specific address. 46 func (w *Wallet) AddressUnconfirmedTransactions(uh types.UnlockHash) (pts []modules.ProcessedTransaction, err error) { 47 if err := w.tg.Add(); err != nil { 48 return []modules.ProcessedTransaction{}, err 49 } 50 defer w.tg.Done() 51 // ensure durability of reported transactions 52 w.mu.Lock() 53 defer w.mu.Unlock() 54 if err = w.syncDB(); err != nil { 55 return 56 } 57 58 // Scan the full list of unconfirmed transactions to see if there are any 59 // related transactions. 60 for _, pt := range w.unconfirmedProcessedTransactions { 61 relevant := false 62 for _, input := range pt.Inputs { 63 if input.RelatedAddress == uh { 64 relevant = true 65 break 66 } 67 } 68 for _, output := range pt.Outputs { 69 if output.RelatedAddress == uh { 70 relevant = true 71 break 72 } 73 } 74 if relevant { 75 pts = append(pts, pt) 76 } 77 } 78 return pts, err 79 } 80 81 // Transaction returns the transaction with the given id. 'False' is returned 82 // if the transaction does not exist. 83 func (w *Wallet) Transaction(txid types.TransactionID) (pt modules.ProcessedTransaction, found bool, err error) { 84 if err := w.tg.Add(); err != nil { 85 return modules.ProcessedTransaction{}, false, err 86 } 87 defer w.tg.Done() 88 // ensure durability of reported transaction 89 w.mu.Lock() 90 defer w.mu.Unlock() 91 if err = w.syncDB(); err != nil { 92 return 93 } 94 95 // Get the keyBytes for the given txid 96 keyBytes, err := dbGetTransactionIndex(w.dbTx, txid) 97 if err != nil { 98 for _, txn := range w.unconfirmedProcessedTransactions { 99 if txn.TransactionID == txid { 100 return txn, true, nil 101 } 102 } 103 return modules.ProcessedTransaction{}, false, nil 104 } 105 106 // Retrieve the transaction 107 found = encoding.Unmarshal(w.dbTx.Bucket(bucketProcessedTransactions).Get(keyBytes), &pt) == nil 108 return 109 } 110 111 // Transactions returns all transactions relevant to the wallet that were 112 // confirmed in the range [startHeight, endHeight]. 113 func (w *Wallet) Transactions(startHeight, endHeight types.BlockHeight) (pts []modules.ProcessedTransaction, err error) { 114 if err := w.tg.Add(); err != nil { 115 return nil, err 116 } 117 defer w.tg.Done() 118 119 // There may be transactions which haven't been saved / committed yet. Sync 120 // the database to ensure that any information which gets reported to the 121 // user will be persisted through a restart. 122 w.mu.Lock() 123 defer w.mu.Unlock() 124 if err = w.syncDB(); err != nil { 125 return nil, err 126 } 127 128 height, err := dbGetConsensusHeight(w.dbTx) 129 if err != nil { 130 return 131 } else if startHeight > height || startHeight > endHeight { 132 return nil, errOutOfBounds 133 } 134 135 // Get the bucket, the largest key in it and the cursor 136 bucket := w.dbTx.Bucket(bucketProcessedTransactions) 137 cursor := bucket.Cursor() 138 nextKey := bucket.Sequence() + 1 139 140 // Database is empty 141 if nextKey == 1 { 142 return 143 } 144 145 var pt modules.ProcessedTransaction 146 keyBytes := make([]byte, 8) 147 var result int 148 func() { 149 // Recover from possible panic during binary search 150 defer func() { 151 r := recover() 152 if r != nil { 153 err = fmt.Errorf("%v", r) 154 } 155 }() 156 157 // Start binary searching 158 result = sort.Search(int(nextKey), func(i int) bool { 159 // Create the key for the index 160 binary.BigEndian.PutUint64(keyBytes, uint64(i)) 161 162 // Retrieve the processed transaction 163 key, ptBytes := cursor.Seek(keyBytes) 164 if build.DEBUG && key == nil { 165 panic("Failed to retrieve processed Transaction by key") 166 } 167 168 // Decode the transaction 169 if err = decodeProcessedTransaction(ptBytes, &pt); build.DEBUG && err != nil { 170 panic(err) 171 } 172 173 return pt.ConfirmationHeight >= startHeight 174 }) 175 }() 176 if err != nil { 177 return 178 } 179 180 if uint64(result) == nextKey { 181 // No transaction was found 182 return 183 } 184 185 // Create the key that corresponds to the result of the search 186 binary.BigEndian.PutUint64(keyBytes, uint64(result)) 187 188 // Get the processed transaction and decode it 189 key, ptBytes := cursor.Seek(keyBytes) 190 if build.DEBUG && key == nil { 191 build.Critical("Couldn't find the processed transaction from the search.") 192 } 193 if err = decodeProcessedTransaction(ptBytes, &pt); build.DEBUG && err != nil { 194 build.Critical(err) 195 } 196 197 // Gather all transactions until endHeight is reached 198 for pt.ConfirmationHeight <= endHeight { 199 if build.DEBUG && pt.ConfirmationHeight < startHeight { 200 build.Critical("wallet processed transactions are not sorted") 201 } 202 pts = append(pts, pt) 203 204 // Get next processed transaction 205 key, ptBytes := cursor.Next() 206 if key == nil { 207 break 208 } 209 210 // Decode the transaction 211 if err := decodeProcessedTransaction(ptBytes, &pt); build.DEBUG && err != nil { 212 panic("Failed to decode the processed transaction") 213 } 214 } 215 return 216 } 217 218 // ComputeValuedTransactions creates ValuedTransaction from a set of 219 // ProcessedTransactions. 220 func ComputeValuedTransactions(pts []modules.ProcessedTransaction, blockHeight types.BlockHeight) ([]modules.ValuedTransaction, error) { 221 // Loop over all transactions and map the id of each contract to the most 222 // recent revision within the set. 223 revisionMap := make(map[types.FileContractID]types.FileContractRevision) 224 for _, pt := range pts { 225 for _, rev := range pt.Transaction.FileContractRevisions { 226 revisionMap[rev.ParentID] = rev 227 } 228 } 229 sts := make([]modules.ValuedTransaction, 0, len(pts)) 230 for _, pt := range pts { 231 // Determine the value of the transaction assuming that it's a regular 232 // transaction. 233 var outgoingSiacoins types.Currency 234 for _, input := range pt.Inputs { 235 if input.FundType == types.SpecifierSiacoinInput && input.WalletAddress { 236 outgoingSiacoins = outgoingSiacoins.Add(input.Value) 237 } 238 } 239 var incomingSiacoins types.Currency 240 for _, output := range pt.Outputs { 241 if output.FundType == types.SpecifierMinerPayout && output.WalletAddress { 242 incomingSiacoins = incomingSiacoins.Add(output.Value) 243 } 244 if output.FundType == types.SpecifierSiacoinOutput && output.WalletAddress { 245 incomingSiacoins = incomingSiacoins.Add(output.Value) 246 } 247 } 248 // Create the txn assuming that it's a regular txn without contracts or 249 // revisions. 250 st := modules.ValuedTransaction{ 251 ProcessedTransaction: pt, 252 ConfirmedIncomingValue: incomingSiacoins, 253 ConfirmedOutgoingValue: outgoingSiacoins, 254 } 255 // If the transaction doesn't contain contracts or revisions we are done. 256 if len(pt.Transaction.FileContracts) == 0 && len(pt.Transaction.FileContractRevisions) == 0 { 257 sts = append(sts, st) 258 continue 259 } 260 // If there are contracts, then there can't be revisions in the 261 // transaction. 262 if len(pt.Transaction.FileContracts) > 0 { 263 // A contract doesn't generate incoming value for the wallet. 264 st.ConfirmedIncomingValue = types.ZeroCurrency 265 // A contract with a revision doesn't have outgoing value since the 266 // outgoing value is determined by the latest revision. 267 _, hasRevision := revisionMap[pt.Transaction.FileContractID(0)] 268 if hasRevision { 269 st.ConfirmedOutgoingValue = types.ZeroCurrency 270 } 271 sts = append(sts, st) 272 continue 273 } 274 // Else the contract contains a revision. 275 rev := pt.Transaction.FileContractRevisions[0] 276 latestRev, ok := revisionMap[rev.ParentID] 277 if !ok { 278 err := errors.New("no revision exists for the provided id which should never happen") 279 build.Critical(err) 280 return nil, err 281 } 282 // If the revision isn't the latest one, it has neither incoming nor 283 // outgoing value. 284 if rev.NewRevisionNumber != latestRev.NewRevisionNumber { 285 st.ConfirmedIncomingValue = types.ZeroCurrency 286 st.ConfirmedOutgoingValue = types.ZeroCurrency 287 sts = append(sts, st) 288 continue 289 } 290 // It is the latest but if it hasn't reached maturiy height yet we 291 // don't count the incoming value. 292 if blockHeight <= rev.NewWindowEnd+types.MaturityDelay { 293 st.ConfirmedIncomingValue = types.ZeroCurrency 294 sts = append(sts, st) 295 continue 296 } 297 // Otherwise we leave the incoming and outgoing value fields the way 298 // they are. 299 sts = append(sts, st) 300 continue 301 } 302 return sts, nil 303 } 304 305 // UnconfirmedTransactions returns the set of unconfirmed transactions that are 306 // relevant to the wallet. 307 func (w *Wallet) UnconfirmedTransactions() ([]modules.ProcessedTransaction, error) { 308 if err := w.tg.Add(); err != nil { 309 return nil, err 310 } 311 defer w.tg.Done() 312 w.mu.RLock() 313 defer w.mu.RUnlock() 314 return w.unconfirmedProcessedTransactions, nil 315 }