decred.org/dcrwallet/v3@v3.1.0/wallet/notifications.go (about) 1 // Copyright (c) 2015-2016 The btcsuite developers 2 // Copyright (c) 2016-2021 The Decred developers 3 // Use of this source code is governed by an ISC 4 // license that can be found in the LICENSE file. 5 6 package wallet 7 8 import ( 9 "bytes" 10 "context" 11 "sync" 12 13 "decred.org/dcrwallet/v3/errors" 14 "decred.org/dcrwallet/v3/internal/compat" 15 "decred.org/dcrwallet/v3/wallet/udb" 16 "decred.org/dcrwallet/v3/wallet/walletdb" 17 "github.com/decred/dcrd/blockchain/stake/v5" 18 "github.com/decred/dcrd/chaincfg/chainhash" 19 "github.com/decred/dcrd/dcrutil/v4" 20 "github.com/decred/dcrd/hdkeychain/v3" 21 "github.com/decred/dcrd/txscript/v4/stdaddr" 22 "github.com/decred/dcrd/txscript/v4/stdscript" 23 "github.com/decred/dcrd/wire" 24 ) 25 26 // TODO: It would be good to send errors during notification creation to the rpc 27 // server instead of just logging them here so the client is aware that wallet 28 // isn't working correctly and notifications are missing. 29 30 // TODO: Anything dealing with accounts here is expensive because the database 31 // is not organized correctly for true account support, but do the slow thing 32 // instead of the easy thing since the db can be fixed later, and we want the 33 // api correct now. 34 35 // NotificationServer is a server that interested clients may hook into to 36 // receive notifications of changes in a wallet. A client is created for each 37 // registered notification. Clients are guaranteed to receive messages in the 38 // order wallet created them, but there is no guaranteed synchronization between 39 // different clients. 40 type NotificationServer struct { 41 transactions []chan *TransactionNotifications 42 // Coalesce transaction notifications since wallet previously did not add 43 // mined txs together. Now it does and this can be rewritten. 44 currentTxNtfn *TransactionNotifications 45 accountClients []chan *AccountNotification 46 tipChangedClients []chan *MainTipChangedNotification 47 confClients []*ConfirmationNotificationsClient 48 removedTransactionClients []chan *RemovedTransactionNotification 49 mu sync.Mutex // Only protects registered clients 50 wallet *Wallet // smells like hacks 51 } 52 53 func newNotificationServer(wallet *Wallet) *NotificationServer { 54 return &NotificationServer{ 55 wallet: wallet, 56 } 57 } 58 59 func lookupInputAccount(dbtx walletdb.ReadTx, w *Wallet, details *udb.TxDetails, deb udb.DebitRecord) uint32 { 60 addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) 61 txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) 62 63 // TODO: Debits should record which account(s?) they 64 // debit from so this doesn't need to be looked up. 65 prevOP := &details.MsgTx.TxIn[deb.Index].PreviousOutPoint 66 prev, err := w.txStore.TxDetails(txmgrNs, &prevOP.Hash) 67 if err != nil { 68 log.Errorf("Cannot query previous transaction details for %v: %v", prevOP.Hash, err) 69 return 0 70 } 71 if prev == nil { 72 log.Errorf("Missing previous transaction %v", prevOP.Hash) 73 return 0 74 } 75 prevOut := prev.MsgTx.TxOut[prevOP.Index] 76 _, addrs := stdscript.ExtractAddrs(prevOut.Version, prevOut.PkScript, w.chainParams) 77 if len(addrs) == 0 { 78 return 0 79 } 80 81 inputAcct, err := w.manager.AddrAccount(addrmgrNs, addrs[0]) 82 if err != nil { 83 log.Errorf("Cannot fetch account for previous output %v: %v", prevOP, err) 84 return 0 85 } 86 return inputAcct 87 } 88 89 func lookupOutputChain(dbtx walletdb.ReadTx, w *Wallet, details *udb.TxDetails, 90 cred udb.CreditRecord) (account uint32, internal bool, address stdaddr.Address, 91 amount int64, outputScript []byte) { 92 93 addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) 94 95 output := details.MsgTx.TxOut[cred.Index] 96 _, addrs := stdscript.ExtractAddrs(output.Version, output.PkScript, w.chainParams) 97 if len(addrs) == 0 { 98 return 99 } 100 101 ma, err := w.manager.Address(addrmgrNs, addrs[0]) 102 if err != nil { 103 log.Errorf("Cannot fetch account for wallet output: %v", err) 104 return 105 } 106 account = ma.Account() 107 internal = ma.Internal() 108 address = ma.Address() 109 amount = output.Value 110 outputScript = output.PkScript 111 return 112 } 113 114 func makeTxSummary(dbtx walletdb.ReadTx, w *Wallet, details *udb.TxDetails) TransactionSummary { 115 serializedTx := details.SerializedTx 116 if serializedTx == nil { 117 var buf bytes.Buffer 118 buf.Grow(details.MsgTx.SerializeSize()) 119 err := details.MsgTx.Serialize(&buf) 120 if err != nil { 121 log.Errorf("Transaction serialization: %v", err) 122 } 123 serializedTx = buf.Bytes() 124 } 125 var fee dcrutil.Amount 126 if len(details.Debits) == len(details.MsgTx.TxIn) { 127 for _, deb := range details.Debits { 128 fee += deb.Amount 129 } 130 for _, txOut := range details.MsgTx.TxOut { 131 fee -= dcrutil.Amount(txOut.Value) 132 } 133 } 134 var inputs []TransactionSummaryInput 135 if len(details.Debits) != 0 { 136 inputs = make([]TransactionSummaryInput, len(details.Debits)) 137 for i, d := range details.Debits { 138 inputs[i] = TransactionSummaryInput{ 139 Index: d.Index, 140 PreviousAccount: lookupInputAccount(dbtx, w, details, d), 141 PreviousAmount: d.Amount, 142 } 143 } 144 } 145 outputs := make([]TransactionSummaryOutput, 0, len(details.MsgTx.TxOut)) 146 for i := range details.MsgTx.TxOut { 147 credIndex := len(outputs) 148 mine := len(details.Credits) > credIndex && details.Credits[credIndex].Index == uint32(i) 149 if !mine { 150 continue 151 } 152 acct, internal, address, amount, outputScript := lookupOutputChain(dbtx, w, details, details.Credits[credIndex]) 153 output := TransactionSummaryOutput{ 154 Index: uint32(i), 155 Account: acct, 156 Internal: internal, 157 Amount: dcrutil.Amount(amount), 158 Address: address, 159 OutputScript: outputScript, 160 } 161 outputs = append(outputs, output) 162 } 163 164 var transactionType = TxTransactionType(&details.MsgTx) 165 166 // Use earliest of receive time or block time if the transaction is mined. 167 receiveTime := details.Received 168 if details.Height() >= 0 && details.Block.Time.Before(receiveTime) { 169 receiveTime = details.Block.Time 170 } 171 172 return TransactionSummary{ 173 Hash: &details.Hash, 174 Transaction: serializedTx, 175 MyInputs: inputs, 176 MyOutputs: outputs, 177 Fee: fee, 178 Timestamp: receiveTime.Unix(), 179 Type: transactionType, 180 } 181 } 182 183 func totalBalances(dbtx walletdb.ReadTx, w *Wallet, m map[uint32]dcrutil.Amount) error { 184 addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) 185 unspent, err := w.txStore.UnspentOutputs(dbtx) 186 if err != nil { 187 return err 188 } 189 for i := range unspent { 190 output := unspent[i] 191 _, addrs := stdscript.ExtractAddrs(scriptVersionAssumed, output.PkScript, w.chainParams) 192 if len(addrs) == 0 { 193 continue 194 } 195 outputAcct, err := w.manager.AddrAccount(addrmgrNs, addrs[0]) 196 if err == nil { 197 _, ok := m[outputAcct] 198 if ok { 199 m[outputAcct] += output.Amount 200 } 201 } 202 } 203 return nil 204 } 205 206 func flattenBalanceMap(m map[uint32]dcrutil.Amount) []AccountBalance { 207 s := make([]AccountBalance, 0, len(m)) 208 for k, v := range m { 209 s = append(s, AccountBalance{Account: k, TotalBalance: v}) 210 } 211 return s 212 } 213 214 func relevantAccounts(w *Wallet, m map[uint32]dcrutil.Amount, txs []TransactionSummary) { 215 for _, tx := range txs { 216 for _, d := range tx.MyInputs { 217 m[d.PreviousAccount] = 0 218 } 219 for _, c := range tx.MyOutputs { 220 m[c.Account] = 0 221 } 222 } 223 } 224 225 func (s *NotificationServer) notifyUnminedTransaction(dbtx walletdb.ReadTx, details *udb.TxDetails) { 226 defer s.mu.Unlock() 227 s.mu.Lock() 228 229 // Sanity check: should not be currently coalescing a notification for 230 // mined transactions at the same time that an unmined tx is notified. 231 if s.currentTxNtfn != nil { 232 log.Tracef("Notifying unmined tx notification while creating notification for blocks") 233 } 234 235 clients := s.transactions 236 if len(clients) == 0 { 237 return 238 } 239 240 unminedTxs := []TransactionSummary{makeTxSummary(dbtx, s.wallet, details)} 241 unminedHashes, err := s.wallet.txStore.UnminedTxHashes(dbtx.ReadBucket(wtxmgrNamespaceKey)) 242 if err != nil { 243 log.Errorf("Cannot fetch unmined transaction hashes: %v", err) 244 return 245 } 246 bals := make(map[uint32]dcrutil.Amount) 247 relevantAccounts(s.wallet, bals, unminedTxs) 248 err = totalBalances(dbtx, s.wallet, bals) 249 if err != nil { 250 log.Errorf("Cannot determine balances for relevant accounts: %v", err) 251 return 252 } 253 n := &TransactionNotifications{ 254 UnminedTransactions: unminedTxs, 255 UnminedTransactionHashes: unminedHashes, 256 NewBalances: flattenBalanceMap(bals), 257 } 258 for _, c := range clients { 259 c <- n 260 } 261 } 262 263 func (s *NotificationServer) notifyDetachedBlock(header *wire.BlockHeader) { 264 defer s.mu.Unlock() 265 s.mu.Lock() 266 267 if s.currentTxNtfn == nil { 268 s.currentTxNtfn = &TransactionNotifications{} 269 } 270 s.currentTxNtfn.DetachedBlocks = append(s.currentTxNtfn.DetachedBlocks, header) 271 } 272 273 func (s *NotificationServer) notifyMinedTransaction(dbtx walletdb.ReadTx, details *udb.TxDetails, block *udb.BlockMeta) { 274 defer s.mu.Unlock() 275 s.mu.Lock() 276 277 if s.currentTxNtfn == nil { 278 s.currentTxNtfn = &TransactionNotifications{} 279 } 280 n := len(s.currentTxNtfn.AttachedBlocks) 281 if n == 0 || s.currentTxNtfn.AttachedBlocks[n-1].Header.BlockHash() != block.Hash { 282 return 283 } 284 txs := &s.currentTxNtfn.AttachedBlocks[n-1].Transactions 285 *txs = append(*txs, makeTxSummary(dbtx, s.wallet, details)) 286 } 287 288 func (s *NotificationServer) notifyAttachedBlock(dbtx walletdb.ReadTx, block *wire.BlockHeader, blockHash *chainhash.Hash) { 289 defer s.mu.Unlock() 290 s.mu.Lock() 291 292 if s.currentTxNtfn == nil { 293 s.currentTxNtfn = &TransactionNotifications{} 294 } 295 296 // Add block details if it wasn't already included for previously 297 // notified mined transactions. 298 n := len(s.currentTxNtfn.AttachedBlocks) 299 if n == 0 || s.currentTxNtfn.AttachedBlocks[n-1].Header.BlockHash() != *blockHash { 300 s.currentTxNtfn.AttachedBlocks = append(s.currentTxNtfn.AttachedBlocks, Block{ 301 Header: block, 302 }) 303 } 304 } 305 306 func (s *NotificationServer) sendAttachedBlockNotification(ctx context.Context) { 307 // Avoid work if possible 308 s.mu.Lock() 309 if len(s.transactions) == 0 { 310 s.currentTxNtfn = nil 311 s.mu.Unlock() 312 return 313 } 314 currentTxNtfn := s.currentTxNtfn 315 s.currentTxNtfn = nil 316 s.mu.Unlock() 317 318 // The UnminedTransactions field is intentionally not set. Since the 319 // hashes of all detached blocks are reported, and all transactions 320 // moved from a mined block back to unconfirmed are either in the 321 // UnminedTransactionHashes slice or don't exist due to conflicting with 322 // a mined transaction in the new best chain, there is no possiblity of 323 // a new, previously unseen transaction appearing in unconfirmed. 324 325 var ( 326 w = s.wallet 327 bals = make(map[uint32]dcrutil.Amount) 328 unminedHashes []*chainhash.Hash 329 ) 330 err := walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error { 331 txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) 332 var err error 333 unminedHashes, err = w.txStore.UnminedTxHashes(txmgrNs) 334 if err != nil { 335 return err 336 } 337 for _, b := range currentTxNtfn.AttachedBlocks { 338 relevantAccounts(w, bals, b.Transactions) 339 } 340 return totalBalances(dbtx, w, bals) 341 342 }) 343 if err != nil { 344 log.Errorf("Failed to construct attached blocks notification: %v", err) 345 return 346 } 347 348 currentTxNtfn.UnminedTransactionHashes = unminedHashes 349 currentTxNtfn.NewBalances = flattenBalanceMap(bals) 350 351 s.mu.Lock() 352 for _, c := range s.transactions { 353 c <- currentTxNtfn 354 } 355 s.mu.Unlock() 356 } 357 358 // TransactionNotifications is a notification of changes to the wallet's 359 // transaction set and the current chain tip that wallet is considered to be 360 // synced with. All transactions added to the blockchain are organized by the 361 // block they were mined in. 362 // 363 // During a chain switch, all removed block hashes are included. Detached 364 // blocks are sorted in the reverse order they were mined. Attached blocks are 365 // sorted in the order mined. 366 // 367 // All newly added unmined transactions are included. Removed unmined 368 // transactions are not explicitly included. Instead, the hashes of all 369 // transactions still unmined are included. 370 // 371 // If any transactions were involved, each affected account's new total balance 372 // is included. 373 // 374 // TODO: Because this includes stuff about blocks and can be fired without any 375 // changes to transactions, it needs a better name. 376 type TransactionNotifications struct { 377 AttachedBlocks []Block 378 DetachedBlocks []*wire.BlockHeader 379 UnminedTransactions []TransactionSummary 380 UnminedTransactionHashes []*chainhash.Hash 381 NewBalances []AccountBalance 382 } 383 384 // Block contains the properties and all relevant transactions of an attached 385 // block. 386 type Block struct { 387 Header *wire.BlockHeader // Nil if referring to mempool 388 Transactions []TransactionSummary 389 } 390 391 // TransactionSummary contains a transaction relevant to the wallet and marks 392 // which inputs and outputs were relevant. 393 type TransactionSummary struct { 394 Hash *chainhash.Hash 395 Transaction []byte 396 MyInputs []TransactionSummaryInput 397 MyOutputs []TransactionSummaryOutput 398 Fee dcrutil.Amount 399 Timestamp int64 400 Type TransactionType 401 } 402 403 // TransactionType describes the which type of transaction is has been observed to be. 404 // For instance, if it has a ticket as an input and a stake base reward as an output, 405 // it is known to be a vote. 406 type TransactionType int8 407 408 const ( 409 // TransactionTypeRegular transaction type for all regular transactions. 410 TransactionTypeRegular TransactionType = iota 411 412 // TransactionTypeCoinbase is the transaction type for all coinbase transactions. 413 TransactionTypeCoinbase 414 415 // TransactionTypeTicketPurchase transaction type for all transactions that 416 // consume regular transactions as inputs and have commitments for future votes 417 // as outputs. 418 TransactionTypeTicketPurchase 419 420 // TransactionTypeVote transaction type for all transactions that consume a ticket 421 // and also offer a stake base reward output. 422 TransactionTypeVote 423 424 // TransactionTypeRevocation transaction type for all transactions that consume a 425 // ticket, but offer no stake base reward. 426 TransactionTypeRevocation 427 ) 428 429 // TxTransactionType returns the correct TransactionType given a wire transaction 430 func TxTransactionType(tx *wire.MsgTx) TransactionType { 431 if compat.IsEitherCoinBaseTx(tx) { 432 return TransactionTypeCoinbase 433 } else if stake.IsSStx(tx) { 434 return TransactionTypeTicketPurchase 435 } else if stake.IsSSGen(tx) { 436 return TransactionTypeVote 437 } else if isRevocation(tx) { 438 return TransactionTypeRevocation 439 } else { 440 return TransactionTypeRegular 441 } 442 } 443 444 // TransactionSummaryInput describes a transaction input that is relevant to the 445 // wallet. The Index field marks the transaction input index of the transaction 446 // (not included here). The PreviousAccount and PreviousAmount fields describe 447 // how much this input debits from a wallet account. 448 type TransactionSummaryInput struct { 449 Index uint32 450 PreviousAccount uint32 451 PreviousAmount dcrutil.Amount 452 } 453 454 // TransactionSummaryOutput describes wallet properties of a transaction output 455 // controlled by the wallet. The Index field marks the transaction output index 456 // of the transaction (not included here). 457 type TransactionSummaryOutput struct { 458 Index uint32 459 Account uint32 460 Internal bool 461 Amount dcrutil.Amount 462 Address stdaddr.Address 463 OutputScript []byte 464 } 465 466 // AccountBalance associates a total (zero confirmation) balance with an 467 // account. Balances for other minimum confirmation counts require more 468 // expensive logic and it is not clear which minimums a client is interested in, 469 // so they are not included. 470 type AccountBalance struct { 471 Account uint32 472 TotalBalance dcrutil.Amount 473 } 474 475 // TransactionNotificationsClient receives TransactionNotifications from the 476 // NotificationServer over the channel C. 477 type TransactionNotificationsClient struct { 478 C <-chan *TransactionNotifications 479 server *NotificationServer 480 } 481 482 // TransactionNotifications returns a client for receiving 483 // TransactionNotifiations notifications over a channel. The channel is 484 // unbuffered. 485 // 486 // When finished, the Done method should be called on the client to disassociate 487 // it from the server. 488 func (s *NotificationServer) TransactionNotifications() TransactionNotificationsClient { 489 c := make(chan *TransactionNotifications) 490 s.mu.Lock() 491 s.transactions = append(s.transactions, c) 492 s.mu.Unlock() 493 return TransactionNotificationsClient{ 494 C: c, 495 server: s, 496 } 497 } 498 499 // Done deregisters the client from the server and drains any remaining 500 // messages. It must be called exactly once when the client is finished 501 // receiving notifications. 502 func (c *TransactionNotificationsClient) Done() { 503 go func() { 504 // Drain notifications until the client channel is removed from 505 // the server and closed. 506 for range c.C { 507 } 508 }() 509 go func() { 510 s := c.server 511 s.mu.Lock() 512 clients := s.transactions 513 for i, ch := range clients { 514 if c.C == ch { 515 clients[i] = clients[len(clients)-1] 516 s.transactions = clients[:len(clients)-1] 517 close(ch) 518 break 519 } 520 } 521 s.mu.Unlock() 522 }() 523 } 524 525 // RemovedTransactionNotification includes the removed transaction hash. 526 type RemovedTransactionNotification struct { 527 TxHash chainhash.Hash 528 } 529 530 // RemovedTransactionNotificationsClient receives RemovedTransactionNotifications over the channel C. 531 type RemovedTransactionNotificationsClient struct { 532 C chan *RemovedTransactionNotification 533 server *NotificationServer 534 } 535 536 // RemovedTransactionNotifications returns a client for receiving RemovedTransactionNotifications over 537 // a channel. The channel is unbuffered. When finished, the client's Done 538 // method should be called to disassociate the client from the server. 539 func (s *NotificationServer) RemovedTransactionNotifications() RemovedTransactionNotificationsClient { 540 c := make(chan *RemovedTransactionNotification) 541 s.mu.Lock() 542 s.removedTransactionClients = append(s.removedTransactionClients, c) 543 s.mu.Unlock() 544 return RemovedTransactionNotificationsClient{ 545 C: c, 546 server: s, 547 } 548 } 549 550 // Done deregisters the client from the server and drains any remaining 551 // messages. It must be called exactly once when the client is finished 552 // receiving notifications. 553 func (c *RemovedTransactionNotificationsClient) Done() { 554 go func() { 555 for range c.C { 556 } 557 }() 558 go func() { 559 s := c.server 560 s.mu.Lock() 561 clients := s.removedTransactionClients 562 for i, ch := range clients { 563 if c.C == ch { 564 clients[i] = clients[len(clients)-1] 565 s.removedTransactionClients = clients[:len(clients)-1] 566 close(ch) 567 break 568 } 569 } 570 s.mu.Unlock() 571 }() 572 } 573 574 func (s *NotificationServer) notifyRemovedTransaction(hash chainhash.Hash) { 575 defer s.mu.Unlock() 576 s.mu.Lock() 577 clients := s.removedTransactionClients 578 if len(clients) == 0 { 579 return 580 } 581 n := &RemovedTransactionNotification{ 582 TxHash: hash, 583 } 584 for _, c := range clients { 585 c <- n 586 } 587 } 588 589 // AccountNotification contains properties regarding an account, such as its 590 // name and the number of derived and imported keys. When any of these 591 // properties change, the notification is fired. 592 type AccountNotification struct { 593 AccountNumber uint32 594 AccountName string 595 ExternalKeyCount uint32 596 InternalKeyCount uint32 597 ImportedKeyCount uint32 598 } 599 600 func (s *NotificationServer) notifyAccountProperties(props *udb.AccountProperties) { 601 defer s.mu.Unlock() 602 s.mu.Lock() 603 clients := s.accountClients 604 if len(clients) == 0 { 605 return 606 } 607 n := &AccountNotification{ 608 AccountNumber: props.AccountNumber, 609 AccountName: props.AccountName, 610 ExternalKeyCount: 0, 611 InternalKeyCount: 0, 612 ImportedKeyCount: props.ImportedKeyCount, 613 } 614 // Key counts have to be fudged for BIP0044 accounts a little bit because 615 // only the last used child index is saved. Add the gap limit since these 616 // addresses have also been generated and are being watched for transaction 617 // activity. 618 if props.AccountNumber <= udb.MaxAccountNum { 619 n.ExternalKeyCount = minUint32(hdkeychain.HardenedKeyStart, 620 props.LastUsedExternalIndex+s.wallet.gapLimit) 621 n.InternalKeyCount = minUint32(hdkeychain.HardenedKeyStart, 622 props.LastUsedInternalIndex+s.wallet.gapLimit) 623 } 624 for _, c := range clients { 625 c <- n 626 } 627 } 628 629 // AccountNotificationsClient receives AccountNotifications over the channel C. 630 type AccountNotificationsClient struct { 631 C chan *AccountNotification 632 server *NotificationServer 633 } 634 635 // AccountNotifications returns a client for receiving AccountNotifications over 636 // a channel. The channel is unbuffered. When finished, the client's Done 637 // method should be called to disassociate the client from the server. 638 func (s *NotificationServer) AccountNotifications() AccountNotificationsClient { 639 c := make(chan *AccountNotification) 640 s.mu.Lock() 641 s.accountClients = append(s.accountClients, c) 642 s.mu.Unlock() 643 return AccountNotificationsClient{ 644 C: c, 645 server: s, 646 } 647 } 648 649 // Done deregisters the client from the server and drains any remaining 650 // messages. It must be called exactly once when the client is finished 651 // receiving notifications. 652 func (c *AccountNotificationsClient) Done() { 653 go func() { 654 for range c.C { 655 } 656 }() 657 go func() { 658 s := c.server 659 s.mu.Lock() 660 clients := s.accountClients 661 for i, ch := range clients { 662 if c.C == ch { 663 clients[i] = clients[len(clients)-1] 664 s.accountClients = clients[:len(clients)-1] 665 close(ch) 666 break 667 } 668 } 669 s.mu.Unlock() 670 }() 671 } 672 673 // MainTipChangedNotification describes processed changes to the main chain tip 674 // block. Attached and detached blocks are sorted by increasing heights. 675 // 676 // This is intended to be a lightweight alternative to TransactionNotifications 677 // when only info regarding the main chain tip block changing is needed. 678 type MainTipChangedNotification struct { 679 AttachedBlocks []*chainhash.Hash 680 DetachedBlocks []*chainhash.Hash 681 NewHeight int32 682 } 683 684 // MainTipChangedNotificationsClient receives MainTipChangedNotifications over 685 // the channel C. 686 type MainTipChangedNotificationsClient struct { 687 C chan *MainTipChangedNotification 688 server *NotificationServer 689 } 690 691 // MainTipChangedNotifications returns a client for receiving 692 // MainTipChangedNotification over a channel. The channel is unbuffered. When 693 // finished, the client's Done method should be called to disassociate the 694 // client from the server. 695 func (s *NotificationServer) MainTipChangedNotifications() MainTipChangedNotificationsClient { 696 c := make(chan *MainTipChangedNotification) 697 s.mu.Lock() 698 s.tipChangedClients = append(s.tipChangedClients, c) 699 s.mu.Unlock() 700 return MainTipChangedNotificationsClient{ 701 C: c, 702 server: s, 703 } 704 } 705 706 // Done deregisters the client from the server and drains any remaining 707 // messages. It must be called exactly once when the client is finished 708 // receiving notifications. 709 func (c *MainTipChangedNotificationsClient) Done() { 710 go func() { 711 for range c.C { 712 } 713 }() 714 go func() { 715 s := c.server 716 s.mu.Lock() 717 clients := s.tipChangedClients 718 for i, ch := range clients { 719 if c.C == ch { 720 clients[i] = clients[len(clients)-1] 721 s.tipChangedClients = clients[:len(clients)-1] 722 close(ch) 723 break 724 } 725 } 726 s.mu.Unlock() 727 }() 728 } 729 730 func (s *NotificationServer) notifyMainChainTipChanged(n *MainTipChangedNotification) { 731 s.mu.Lock() 732 733 for _, c := range s.tipChangedClients { 734 c <- n 735 } 736 737 if len(s.confClients) > 0 { 738 var wg sync.WaitGroup 739 wg.Add(len(s.confClients)) 740 for _, c := range s.confClients { 741 c := c 742 go func() { 743 c.process(n.NewHeight) 744 wg.Done() 745 }() 746 } 747 wg.Wait() 748 } 749 750 s.mu.Unlock() 751 } 752 753 // ConfirmationNotifications registers a client for confirmation notifications 754 // from the notification server. 755 func (s *NotificationServer) ConfirmationNotifications(ctx context.Context) *ConfirmationNotificationsClient { 756 c := &ConfirmationNotificationsClient{ 757 watched: make(map[chainhash.Hash]int32), 758 r: make(chan *confNtfnResult), 759 ctx: ctx, 760 s: s, 761 } 762 763 // Register with the server 764 s.mu.Lock() 765 s.confClients = append(s.confClients, c) 766 s.mu.Unlock() 767 768 // Cleanup when caller signals done. 769 go func() { 770 <-ctx.Done() 771 772 // Remove item from notification server's slice 773 s.mu.Lock() 774 slice := &s.confClients 775 for i, sc := range *slice { 776 if c == sc { 777 (*slice)[i] = (*slice)[len(*slice)-1] 778 *slice = (*slice)[:len(*slice)-1] 779 break 780 } 781 } 782 s.mu.Unlock() 783 }() 784 785 return c 786 } 787 788 // ConfirmationNotificationsClient provides confirmation notifications of watched 789 // transactions until the caller's context signals done. Callers register for 790 // notifications using Watch and receive notifications by calling Recv. 791 type ConfirmationNotificationsClient struct { 792 watched map[chainhash.Hash]int32 793 mu sync.Mutex 794 795 r chan *confNtfnResult 796 ctx context.Context 797 s *NotificationServer 798 } 799 800 type confNtfnResult struct { 801 result []ConfirmationNotification 802 err error 803 } 804 805 // ConfirmationNotification describes the number of confirmations of a single 806 // transaction, or -1 if the transaction is unknown or removed from the wallet. 807 // If the transaction is mined (Confirmations >= 1), the block hash and height 808 // is included. Otherwise the block hash is nil and the block height is set to 809 // -1. 810 type ConfirmationNotification struct { 811 TxHash *chainhash.Hash 812 Confirmations int32 813 BlockHash *chainhash.Hash // nil when unmined 814 BlockHeight int32 // -1 when unmined 815 } 816 817 // Watch adds additional transactions to watch and create confirmation results 818 // for. Results are immediately created with the current number of 819 // confirmations and are watched until stopAfter confirmations is met or the 820 // transaction is unknown or removed from the wallet. 821 func (c *ConfirmationNotificationsClient) Watch(txHashes []*chainhash.Hash, stopAfter int32) { 822 if len(txHashes) == 0 { 823 return 824 } 825 w := c.s.wallet 826 r := make([]ConfirmationNotification, 0, len(c.watched)) 827 err := walletdb.View(c.ctx, w.db, func(dbtx walletdb.ReadTx) error { 828 txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) 829 _, tipHeight := w.txStore.MainChainTip(dbtx) 830 // cannot range here, txHashes may be modified 831 for i := 0; i < len(txHashes); { 832 h := txHashes[i] 833 height, err := w.txStore.TxBlockHeight(dbtx, h) 834 var confs int32 835 switch { 836 case errors.Is(err, errors.NotExist): 837 confs = -1 838 case err != nil: 839 return err 840 default: 841 // Remove tx hash from watching list if tx block has been mined 842 // and then invalidated by next block 843 if tipHeight > height && height > 0 { 844 txDetails, err := w.txStore.TxDetails(txmgrNs, h) 845 if err != nil { 846 return err 847 } 848 _, invalidated := w.txStore.BlockInMainChain(dbtx, &txDetails.Block.Hash) 849 if invalidated { 850 confs = -1 851 break 852 } 853 } 854 confs = confirms(height, tipHeight) 855 } 856 r = append(r, ConfirmationNotification{ 857 TxHash: h, 858 Confirmations: confs, 859 BlockHeight: -1, 860 }) 861 if confs > 0 { 862 result := &r[len(r)-1] 863 height, err := w.txStore.TxBlockHeight(dbtx, result.TxHash) 864 if err != nil { 865 return err 866 } 867 blockHash, err := w.txStore.GetMainChainBlockHashForHeight(txmgrNs, height) 868 if err != nil { 869 return err 870 } 871 result.BlockHash = &blockHash 872 result.BlockHeight = height 873 } 874 if confs >= stopAfter || confs == -1 { 875 // Remove this hash from the slice so it is not added to the 876 // watch map. Do not increment i so this same index is used 877 // next iteration with the new hash. 878 s := &txHashes 879 (*s)[i] = (*s)[len(*s)-1] 880 *s = (*s)[:len(*s)-1] 881 } else { 882 i++ 883 } 884 } 885 return nil 886 }) 887 if err != nil { 888 r = nil 889 } 890 select { 891 case c.r <- &confNtfnResult{r, err}: 892 case <-c.ctx.Done(): 893 } 894 895 c.mu.Lock() 896 for _, h := range txHashes { 897 c.watched[*h] = stopAfter 898 } 899 c.mu.Unlock() 900 } 901 902 // Recv waits for the next notification. Returns context.Canceled when the 903 // context is canceled. 904 func (c *ConfirmationNotificationsClient) Recv() ([]ConfirmationNotification, error) { 905 select { 906 case <-c.ctx.Done(): 907 return nil, context.Canceled 908 case r := <-c.r: 909 return r.result, r.err 910 } 911 } 912 913 func (c *ConfirmationNotificationsClient) process(tipHeight int32) { 914 select { 915 case <-c.ctx.Done(): 916 return 917 default: 918 } 919 920 c.mu.Lock() 921 w := c.s.wallet 922 r := &confNtfnResult{ 923 result: make([]ConfirmationNotification, 0, len(c.watched)), 924 } 925 var unwatch []*chainhash.Hash 926 err := walletdb.View(c.ctx, w.db, func(dbtx walletdb.ReadTx) error { 927 txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) 928 for txHash, stopAfter := range c.watched { 929 txHash := txHash // copy 930 height, err := w.txStore.TxBlockHeight(dbtx, &txHash) 931 var confs int32 932 switch { 933 case errors.Is(err, errors.NotExist): 934 confs = -1 935 case err != nil: 936 return err 937 default: 938 // Remove tx hash from watching list if tx block has been mined 939 // and then invalidated by next block 940 if tipHeight > height && height > 0 { 941 txDetails, err := w.txStore.TxDetails(txmgrNs, &txHash) 942 if err != nil { 943 return err 944 } 945 _, invalidated := w.txStore.BlockInMainChain(dbtx, &txDetails.Block.Hash) 946 if invalidated { 947 confs = -1 948 break 949 } 950 } 951 confs = confirms(height, tipHeight) 952 } 953 r.result = append(r.result, ConfirmationNotification{ 954 TxHash: &txHash, 955 Confirmations: confs, 956 BlockHeight: -1, 957 }) 958 if confs > 0 { 959 result := &r.result[len(r.result)-1] 960 height, err := w.txStore.TxBlockHeight(dbtx, result.TxHash) 961 if err != nil { 962 return err 963 } 964 blockHash, err := w.txStore.GetMainChainBlockHashForHeight(txmgrNs, height) 965 if err != nil { 966 return err 967 } 968 result.BlockHash = &blockHash 969 result.BlockHeight = height 970 } 971 if confs >= stopAfter || confs == -1 { 972 unwatch = append(unwatch, &txHash) 973 } 974 } 975 return nil 976 }) 977 if err != nil { 978 r.result = nil 979 r.err = err 980 } 981 for _, h := range unwatch { 982 delete(c.watched, *h) 983 } 984 c.mu.Unlock() 985 986 select { 987 case c.r <- r: 988 case <-c.ctx.Done(): 989 } 990 }