2017-11-13 22:55:22 +03:00
|
|
|
package chainntnfs
|
|
|
|
|
|
|
|
import (
|
2018-03-19 22:22:44 +03:00
|
|
|
"errors"
|
2017-11-13 22:55:22 +03:00
|
|
|
"fmt"
|
2018-07-27 07:31:32 +03:00
|
|
|
"sync"
|
2017-11-13 22:55:22 +03:00
|
|
|
|
2018-06-05 04:34:16 +03:00
|
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
|
|
"github.com/btcsuite/btcutil"
|
2017-11-13 22:55:22 +03:00
|
|
|
)
|
|
|
|
|
2018-07-27 07:27:27 +03:00
|
|
|
var (
|
|
|
|
// ErrTxConfNotifierExiting is an error returned when attempting to
|
|
|
|
// interact with the TxConfNotifier but it been shut down.
|
|
|
|
ErrTxConfNotifierExiting = errors.New("TxConfNotifier is exiting")
|
2018-10-18 04:10:08 +03:00
|
|
|
|
|
|
|
// ErrTxMaxConfs signals that the user requested a number of
|
|
|
|
// confirmations beyond the reorg safety limit.
|
|
|
|
ErrTxMaxConfs = errors.New("too many confirmations requested")
|
2018-07-27 07:27:27 +03:00
|
|
|
)
|
|
|
|
|
2017-11-13 22:55:22 +03:00
|
|
|
// ConfNtfn represents a notifier client's request to receive a notification
|
|
|
|
// once the target transaction gets sufficient confirmations. The client is
|
|
|
|
// asynchronously notified via the ConfirmationEvent channels.
|
|
|
|
type ConfNtfn struct {
|
2018-07-27 07:30:15 +03:00
|
|
|
// ConfID uniquely identifies the confirmation notification request for
|
|
|
|
// the specified transaction.
|
|
|
|
ConfID uint64
|
|
|
|
|
2017-12-18 05:40:05 +03:00
|
|
|
// TxID is the hash of the transaction for which confirmation notifications
|
2017-11-13 22:55:22 +03:00
|
|
|
// are requested.
|
|
|
|
TxID *chainhash.Hash
|
|
|
|
|
2018-09-29 02:32:53 +03:00
|
|
|
// PkScript is the public key script of an outpoint created in this
|
|
|
|
// transaction.
|
2018-10-03 23:51:05 +03:00
|
|
|
//
|
|
|
|
// NOTE: This value MUST be set when the dispatch is to be performed
|
|
|
|
// using compact filters.
|
2018-09-29 02:32:53 +03:00
|
|
|
PkScript []byte
|
|
|
|
|
2017-11-13 22:55:22 +03:00
|
|
|
// NumConfirmations is the number of confirmations after which the
|
|
|
|
// notification is to be sent.
|
|
|
|
NumConfirmations uint32
|
|
|
|
|
|
|
|
// Event contains references to the channels that the notifications are to
|
|
|
|
// be sent over.
|
|
|
|
Event *ConfirmationEvent
|
|
|
|
|
2018-08-25 06:09:11 +03:00
|
|
|
// HeightHint is the minimum height in the chain that we expect to find
|
2018-09-29 02:36:47 +03:00
|
|
|
// this txid.
|
2018-08-25 06:09:11 +03:00
|
|
|
HeightHint uint32
|
|
|
|
|
2017-11-13 22:55:22 +03:00
|
|
|
// dispatched is false if the confirmed notification has not been sent yet.
|
|
|
|
dispatched bool
|
|
|
|
}
|
|
|
|
|
2018-09-29 02:24:19 +03:00
|
|
|
// HistoricalConfDispatch parameterizes a manual rescan for a particular
|
|
|
|
// transaction identifier. The parameters include the start and end block
|
|
|
|
// heights specifying the range of blocks to scan.
|
|
|
|
type HistoricalConfDispatch struct {
|
|
|
|
// TxID is the transaction ID to search for in the historical dispatch.
|
|
|
|
TxID *chainhash.Hash
|
|
|
|
|
|
|
|
// PkScript is a public key script from an output created by this
|
|
|
|
// transaction.
|
2018-10-03 23:51:05 +03:00
|
|
|
//
|
|
|
|
// NOTE: This value MUST be set when the dispatch is to be performed
|
|
|
|
// using compact filters.
|
2018-09-29 02:24:19 +03:00
|
|
|
PkScript []byte
|
|
|
|
|
|
|
|
// StartHeight specifies the block height at which to being the
|
|
|
|
// historical rescan.
|
|
|
|
StartHeight uint32
|
|
|
|
|
|
|
|
// EndHeight specifies the last block height (inclusive) that the
|
|
|
|
// historical scan should consider.
|
|
|
|
EndHeight uint32
|
|
|
|
}
|
|
|
|
|
2017-11-13 22:55:22 +03:00
|
|
|
// NewConfirmationEvent constructs a new ConfirmationEvent with newly opened
|
|
|
|
// channels.
|
2018-03-19 21:48:44 +03:00
|
|
|
func NewConfirmationEvent(numConfs uint32) *ConfirmationEvent {
|
2017-11-13 22:55:22 +03:00
|
|
|
return &ConfirmationEvent{
|
|
|
|
Confirmed: make(chan *TxConfirmation, 1),
|
2018-03-19 21:48:44 +03:00
|
|
|
Updates: make(chan uint32, numConfs),
|
2017-11-13 22:55:22 +03:00
|
|
|
NegativeConf: make(chan int32, 1),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TxConfNotifier is used to register transaction confirmation notifications and
|
|
|
|
// dispatch them as the transactions confirm. A client can request to be
|
|
|
|
// notified when a particular transaction has sufficient on-chain confirmations
|
|
|
|
// (or be notified immediately if the tx already does), and the TxConfNotifier
|
|
|
|
// will watch changes to the blockchain in order to satisfy these requests.
|
|
|
|
type TxConfNotifier struct {
|
|
|
|
// currentHeight is the height of the tracked blockchain. It is used to
|
|
|
|
// determine the number of confirmations a tx has and ensure blocks are
|
|
|
|
// connected and disconnected in order.
|
|
|
|
currentHeight uint32
|
|
|
|
|
|
|
|
// reorgSafetyLimit is the chain depth beyond which it is assumed a block
|
|
|
|
// will not be reorganized out of the chain. This is used to determine when
|
|
|
|
// to prune old confirmation requests so that reorgs are handled correctly.
|
|
|
|
// The coinbase maturity period is a reasonable value to use.
|
|
|
|
reorgSafetyLimit uint32
|
|
|
|
|
2017-11-14 04:32:11 +03:00
|
|
|
// reorgDepth is the depth of a chain organization that this system is being
|
|
|
|
// informed of. This is incremented as long as a sequence of blocks are
|
|
|
|
// disconnected without being interrupted by a new block.
|
|
|
|
reorgDepth uint32
|
|
|
|
|
2017-11-13 22:55:22 +03:00
|
|
|
// confNotifications is an index of notification requests by transaction
|
|
|
|
// hash.
|
2018-08-25 05:29:55 +03:00
|
|
|
confNotifications map[chainhash.Hash]*confNtfnSet
|
2017-11-13 22:55:22 +03:00
|
|
|
|
2018-03-19 22:04:19 +03:00
|
|
|
// txsByInitialHeight is an index of watched transactions by the height
|
2017-11-13 22:55:22 +03:00
|
|
|
// that they are included at in the blockchain. This is tracked so that
|
2018-03-19 22:04:19 +03:00
|
|
|
// incorrect notifications are not sent if a transaction is reorganized
|
|
|
|
// out of the chain and so that negative confirmations can be recognized.
|
|
|
|
txsByInitialHeight map[uint32]map[chainhash.Hash]struct{}
|
2017-11-13 22:55:22 +03:00
|
|
|
|
|
|
|
// ntfnsByConfirmHeight is an index of notification requests by the height
|
|
|
|
// at which the transaction will have sufficient confirmations.
|
|
|
|
ntfnsByConfirmHeight map[uint32]map[*ConfNtfn]struct{}
|
2017-12-05 00:30:33 +03:00
|
|
|
|
2018-05-22 22:55:32 +03:00
|
|
|
// hintCache is a cache used to maintain the latest height hints for
|
|
|
|
// transactions. Each height hint represents the earliest height at
|
|
|
|
// which the transactions could have been confirmed within the chain.
|
|
|
|
hintCache ConfirmHintCache
|
|
|
|
|
2017-12-05 00:30:33 +03:00
|
|
|
// quit is closed in order to signal that the notifier is gracefully
|
|
|
|
// exiting.
|
|
|
|
quit chan struct{}
|
2018-07-27 07:31:32 +03:00
|
|
|
|
|
|
|
sync.Mutex
|
2017-11-13 22:55:22 +03:00
|
|
|
}
|
|
|
|
|
2018-09-29 02:25:20 +03:00
|
|
|
// rescanState indicates the progression of a registration before the notifier
|
|
|
|
// can begin dispatching confirmations at tip.
|
|
|
|
type rescanState uint8
|
|
|
|
|
|
|
|
const (
|
|
|
|
// rescanNotStarted is the initial state, denoting that a historical
|
|
|
|
// dispatch may be required.
|
|
|
|
rescanNotStarted rescanState = iota
|
|
|
|
|
|
|
|
// rescanPending indicates that a dispatch has already been made, and we
|
|
|
|
// are waiting for its completion. No other rescans should be dispatched
|
|
|
|
// while in this state.
|
|
|
|
rescanPending
|
|
|
|
|
|
|
|
// rescanComplete signals either that a rescan was dispatched and has
|
|
|
|
// completed, or that we began watching at tip immediately. In either
|
|
|
|
// case, the notifier can only dispatch notifications from tip when in
|
|
|
|
// this state.
|
|
|
|
rescanComplete
|
|
|
|
)
|
|
|
|
|
2018-08-25 05:29:55 +03:00
|
|
|
// confNtfnSet holds all known, registered confirmation notifications for a
|
|
|
|
// single txid. If duplicates notifications are requested, only one historical
|
|
|
|
// dispatch will be spawned to ensure redundant scans are not permitted. A
|
|
|
|
// single conf detail will be constructed and dispatched to all interested
|
|
|
|
// clients.
|
|
|
|
type confNtfnSet struct {
|
|
|
|
ntfns map[uint64]*ConfNtfn
|
|
|
|
rescanStatus rescanState
|
|
|
|
details *TxConfirmation
|
|
|
|
}
|
|
|
|
|
|
|
|
// newConfNtfnSet constructs a fresh confNtfnSet for a group of clients
|
|
|
|
// interested in a notification for a particular txid.
|
|
|
|
func newConfNtfnSet() *confNtfnSet {
|
|
|
|
return &confNtfnSet{
|
|
|
|
ntfns: make(map[uint64]*ConfNtfn),
|
|
|
|
rescanStatus: rescanNotStarted,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-13 22:55:22 +03:00
|
|
|
// NewTxConfNotifier creates a TxConfNotifier. The current height of the
|
|
|
|
// blockchain is accepted as a parameter.
|
2018-05-22 22:55:32 +03:00
|
|
|
func NewTxConfNotifier(startHeight uint32, reorgSafetyLimit uint32,
|
|
|
|
hintCache ConfirmHintCache) *TxConfNotifier {
|
|
|
|
|
2017-11-13 22:55:22 +03:00
|
|
|
return &TxConfNotifier{
|
2018-03-19 22:04:19 +03:00
|
|
|
currentHeight: startHeight,
|
|
|
|
reorgSafetyLimit: reorgSafetyLimit,
|
2018-08-25 05:29:55 +03:00
|
|
|
confNotifications: make(map[chainhash.Hash]*confNtfnSet),
|
2018-03-19 22:04:19 +03:00
|
|
|
txsByInitialHeight: make(map[uint32]map[chainhash.Hash]struct{}),
|
|
|
|
ntfnsByConfirmHeight: make(map[uint32]map[*ConfNtfn]struct{}),
|
2018-05-22 22:55:32 +03:00
|
|
|
hintCache: hintCache,
|
2018-03-19 22:04:19 +03:00
|
|
|
quit: make(chan struct{}),
|
2017-11-13 22:55:22 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Register handles a new notification request. The client will be notified when
|
|
|
|
// the transaction gets a sufficient number of confirmations on the blockchain.
|
2018-09-29 02:36:47 +03:00
|
|
|
// The registration succeeds if no error is returned. If the returned
|
|
|
|
// HistoricalConfDispatch is non-nil, the caller is responsible for attempting
|
|
|
|
// to manually rescan blocks for the txid between the start and end heights.
|
2018-07-27 07:31:32 +03:00
|
|
|
//
|
|
|
|
// NOTE: If the transaction has already been included in a block on the chain,
|
|
|
|
// the confirmation details must be provided with the UpdateConfDetails method,
|
|
|
|
// otherwise we will wait for the transaction to confirm even though it already
|
|
|
|
// has.
|
2018-09-29 02:36:47 +03:00
|
|
|
func (tcn *TxConfNotifier) Register(
|
|
|
|
ntfn *ConfNtfn) (*HistoricalConfDispatch, error) {
|
|
|
|
|
2017-12-05 00:30:33 +03:00
|
|
|
select {
|
|
|
|
case <-tcn.quit:
|
2018-09-29 02:36:47 +03:00
|
|
|
return nil, ErrTxConfNotifierExiting
|
2017-12-05 00:30:33 +03:00
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
2018-10-18 04:10:08 +03:00
|
|
|
// Enforce that we will not dispatch confirmations beyond the reorg
|
|
|
|
// safety limit.
|
|
|
|
if ntfn.NumConfirmations > tcn.reorgSafetyLimit {
|
|
|
|
return nil, ErrTxMaxConfs
|
|
|
|
}
|
|
|
|
|
2018-08-25 06:09:11 +03:00
|
|
|
// Before proceeding to register the notification, we'll query our
|
|
|
|
// height hint cache to determine whether a better one exists.
|
2018-09-29 02:36:47 +03:00
|
|
|
//
|
|
|
|
// TODO(conner): verify that all submitted height hints are identical.
|
2018-09-29 02:36:47 +03:00
|
|
|
startHeight := ntfn.HeightHint
|
2018-08-25 06:09:11 +03:00
|
|
|
hint, err := tcn.hintCache.QueryConfirmHint(*ntfn.TxID)
|
|
|
|
if err == nil {
|
2018-09-29 02:36:47 +03:00
|
|
|
if hint > startHeight {
|
2018-08-25 06:09:11 +03:00
|
|
|
Log.Debugf("Using height hint %d retrieved "+
|
|
|
|
"from cache for %v", hint, *ntfn.TxID)
|
2018-09-29 02:36:47 +03:00
|
|
|
startHeight = hint
|
2018-08-25 06:09:11 +03:00
|
|
|
}
|
2018-09-29 23:55:30 +03:00
|
|
|
} else if err != ErrConfirmHintNotFound {
|
|
|
|
Log.Errorf("Unable to query confirm hint for %v: %v",
|
|
|
|
*ntfn.TxID, err)
|
2018-08-25 06:09:11 +03:00
|
|
|
}
|
|
|
|
|
2018-07-27 07:31:32 +03:00
|
|
|
tcn.Lock()
|
|
|
|
defer tcn.Unlock()
|
|
|
|
|
2018-08-25 05:29:55 +03:00
|
|
|
confSet, ok := tcn.confNotifications[*ntfn.TxID]
|
2018-07-27 07:31:32 +03:00
|
|
|
if !ok {
|
2018-09-29 23:55:30 +03:00
|
|
|
// If this is the first registration for this txid, construct a
|
|
|
|
// confSet to coalesce all notifications for the same txid.
|
2018-08-25 05:29:55 +03:00
|
|
|
confSet = newConfNtfnSet()
|
|
|
|
tcn.confNotifications[*ntfn.TxID] = confSet
|
|
|
|
}
|
2018-05-22 22:55:32 +03:00
|
|
|
|
2018-08-25 05:29:55 +03:00
|
|
|
confSet.ntfns[ntfn.ConfID] = ntfn
|
|
|
|
|
|
|
|
switch confSet.rescanStatus {
|
|
|
|
|
|
|
|
// A prior rescan has already completed and we are actively watching at
|
|
|
|
// tip for this txid.
|
|
|
|
case rescanComplete:
|
2018-09-29 02:30:13 +03:00
|
|
|
// If conf details for this set of notifications has already
|
|
|
|
// been found, we'll attempt to deliver them immediately to this
|
|
|
|
// client.
|
2018-09-29 23:55:30 +03:00
|
|
|
Log.Debugf("Attempting to dispatch conf for txid=%v "+
|
|
|
|
"on registration since rescan has finished", ntfn.TxID)
|
|
|
|
return nil, tcn.dispatchConfDetails(ntfn, confSet.details)
|
2018-08-25 05:29:55 +03:00
|
|
|
|
|
|
|
// A rescan is already in progress, return here to prevent dispatching
|
|
|
|
// another. When the scan returns, this notifications details will be
|
|
|
|
// updated as well.
|
|
|
|
case rescanPending:
|
2018-09-29 23:55:30 +03:00
|
|
|
Log.Debugf("Waiting for pending rescan to finish before "+
|
|
|
|
"notifying txid=%v at tip", ntfn.TxID)
|
2018-08-25 05:29:55 +03:00
|
|
|
return nil, nil
|
|
|
|
|
|
|
|
// If no rescan has been dispatched, attempt to do so now.
|
|
|
|
case rescanNotStarted:
|
2018-07-27 07:31:32 +03:00
|
|
|
}
|
|
|
|
|
2018-08-25 05:29:55 +03:00
|
|
|
// If the provided or cached height hint indicates that the transaction
|
|
|
|
// is to be confirmed at a height greater than the conf notifier's
|
|
|
|
// current height, we'll refrain from spawning a historical dispatch.
|
|
|
|
if startHeight > tcn.currentHeight {
|
2018-09-29 23:55:30 +03:00
|
|
|
Log.Debugf("Height hint is above current height, not dispatching "+
|
|
|
|
"historical rescan for txid=%v ", ntfn.TxID)
|
2018-08-25 05:29:55 +03:00
|
|
|
// Set the rescan status to complete, which will allow the conf
|
|
|
|
// notifier to start delivering messages for this set
|
|
|
|
// immediately.
|
|
|
|
confSet.rescanStatus = rescanComplete
|
|
|
|
return nil, nil
|
|
|
|
}
|
2018-07-27 07:31:32 +03:00
|
|
|
|
2018-09-29 23:55:30 +03:00
|
|
|
Log.Debugf("Dispatching historical rescan for txid=%v ", ntfn.TxID)
|
|
|
|
|
2018-09-29 02:36:47 +03:00
|
|
|
// Construct the parameters for historical dispatch, scanning the range
|
|
|
|
// of blocks between our best known height hint and the notifier's
|
|
|
|
// current height. The notifier will begin also watching for
|
|
|
|
// confirmations at tip starting with the next block.
|
|
|
|
dispatch := &HistoricalConfDispatch{
|
|
|
|
TxID: ntfn.TxID,
|
|
|
|
PkScript: ntfn.PkScript,
|
|
|
|
StartHeight: startHeight,
|
|
|
|
EndHeight: tcn.currentHeight,
|
|
|
|
}
|
|
|
|
|
2018-08-25 05:29:55 +03:00
|
|
|
// Set this confSet's status to pending, ensuring subsequent
|
|
|
|
// registrations don't also attempt a dispatch.
|
|
|
|
confSet.rescanStatus = rescanPending
|
2018-09-29 02:36:47 +03:00
|
|
|
|
|
|
|
return dispatch, nil
|
2018-07-27 07:31:32 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// UpdateConfDetails attempts to update the confirmation details for an active
|
|
|
|
// notification within the notifier. This should only be used in the case of a
|
|
|
|
// transaction that has confirmed before the notifier's current height.
|
|
|
|
//
|
|
|
|
// NOTE: The notification should be registered first to ensure notifications are
|
|
|
|
// dispatched correctly.
|
|
|
|
func (tcn *TxConfNotifier) UpdateConfDetails(txid chainhash.Hash,
|
2018-09-29 02:38:08 +03:00
|
|
|
details *TxConfirmation) error {
|
2018-07-27 07:31:32 +03:00
|
|
|
|
|
|
|
select {
|
|
|
|
case <-tcn.quit:
|
|
|
|
return ErrTxConfNotifierExiting
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure we hold the lock throughout handling the notification to
|
|
|
|
// prevent the notifier from advancing its height underneath us.
|
|
|
|
tcn.Lock()
|
|
|
|
defer tcn.Unlock()
|
|
|
|
|
|
|
|
// First, we'll determine whether we have an active notification for
|
|
|
|
// this transaction with the given ID.
|
2018-08-25 05:29:55 +03:00
|
|
|
confSet, ok := tcn.confNotifications[txid]
|
2018-07-27 07:31:32 +03:00
|
|
|
if !ok {
|
2018-08-25 05:29:55 +03:00
|
|
|
return fmt.Errorf("no notification found with TxID %v", txid)
|
2018-07-27 07:31:32 +03:00
|
|
|
}
|
|
|
|
|
2018-10-18 04:10:08 +03:00
|
|
|
// If the conf details were already found at tip, all existing
|
|
|
|
// notifications will have been dispatched or queued for dispatch. We
|
|
|
|
// can exit early to avoid sending too many notifications on the
|
|
|
|
// buffered channels.
|
|
|
|
if confSet.details != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-08-25 05:29:55 +03:00
|
|
|
// The historical dispatch has been completed for this confSet. We'll
|
|
|
|
// update the rescan status and cache any details that were found. If
|
|
|
|
// the details are nil, that implies we did not find them and will
|
|
|
|
// continue to watch for them at tip.
|
|
|
|
confSet.rescanStatus = rescanComplete
|
2018-07-27 07:31:32 +03:00
|
|
|
|
2018-08-25 05:29:55 +03:00
|
|
|
// The notifier has yet to reach the height at which the transaction was
|
|
|
|
// included in a block, so we should defer until handling it then within
|
|
|
|
// ConnectTip.
|
2018-09-29 23:55:30 +03:00
|
|
|
if details == nil {
|
|
|
|
Log.Debugf("Conf details for txid=%v not found during "+
|
|
|
|
"historical dispatch, waiting to dispatch at tip", txid)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if details.BlockHeight > tcn.currentHeight {
|
|
|
|
Log.Debugf("Conf details for txid=%v found above current "+
|
|
|
|
"height, waiting to dispatch at tip", txid)
|
2018-07-27 07:31:32 +03:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-09-29 23:55:30 +03:00
|
|
|
Log.Debugf("Updating conf details for txid=%v details", txid)
|
|
|
|
|
2018-05-22 22:55:32 +03:00
|
|
|
err := tcn.hintCache.CommitConfirmHint(details.BlockHeight, txid)
|
|
|
|
if err != nil {
|
|
|
|
// The error is not fatal, so we should not return an error to
|
|
|
|
// the caller.
|
|
|
|
Log.Errorf("Unable to update confirm hint to %d for %v: %v",
|
|
|
|
details.BlockHeight, txid, err)
|
|
|
|
}
|
|
|
|
|
2018-09-29 02:30:13 +03:00
|
|
|
// Cache the details found in the rescan and attempt to dispatch any
|
|
|
|
// notifications that have not yet been delivered.
|
|
|
|
confSet.details = details
|
2018-08-25 05:29:55 +03:00
|
|
|
for _, ntfn := range confSet.ntfns {
|
2018-09-29 02:30:13 +03:00
|
|
|
err = tcn.dispatchConfDetails(ntfn, details)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// dispatchConfDetails attempts to cache and dispatch details to a particular
|
|
|
|
// client if the transaction has sufficiently confirmed. If the provided details
|
|
|
|
// are nil, this method will be a no-op.
|
|
|
|
func (tcn *TxConfNotifier) dispatchConfDetails(
|
|
|
|
ntfn *ConfNtfn, details *TxConfirmation) error {
|
|
|
|
|
|
|
|
// If no details are provided, return early as we can't dispatch.
|
|
|
|
if details == nil {
|
2018-09-29 23:55:30 +03:00
|
|
|
Log.Debugf("Unable to dispatch %v, no details provided",
|
|
|
|
ntfn.TxID)
|
2018-09-29 02:30:13 +03:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now, we'll examine whether the transaction of this
|
|
|
|
// notification request has reached its required number of
|
|
|
|
// confirmations. If it has, we'll dispatch a confirmation
|
|
|
|
// notification to the caller.
|
|
|
|
confHeight := details.BlockHeight + ntfn.NumConfirmations - 1
|
|
|
|
if confHeight <= tcn.currentHeight {
|
|
|
|
Log.Infof("Dispatching %v conf notification for %v",
|
|
|
|
ntfn.NumConfirmations, ntfn.TxID)
|
|
|
|
|
|
|
|
// We'll send a 0 value to the Updates channel,
|
|
|
|
// indicating that the transaction has already been
|
|
|
|
// confirmed.
|
|
|
|
select {
|
|
|
|
case ntfn.Event.Updates <- 0:
|
|
|
|
case <-tcn.quit:
|
|
|
|
return ErrTxConfNotifierExiting
|
2018-03-19 22:22:44 +03:00
|
|
|
}
|
|
|
|
|
2018-09-29 02:30:13 +03:00
|
|
|
select {
|
|
|
|
case ntfn.Event.Confirmed <- details:
|
|
|
|
ntfn.dispatched = true
|
|
|
|
case <-tcn.quit:
|
|
|
|
return ErrTxConfNotifierExiting
|
|
|
|
}
|
|
|
|
} else {
|
2018-09-29 23:55:30 +03:00
|
|
|
Log.Debugf("Queueing %v conf notification for %v at tip ",
|
|
|
|
ntfn.NumConfirmations, ntfn.TxID)
|
|
|
|
|
2018-09-29 02:30:13 +03:00
|
|
|
// Otherwise, we'll keep track of the notification
|
|
|
|
// request by the height at which we should dispatch the
|
|
|
|
// confirmation notification.
|
|
|
|
ntfnSet, exists := tcn.ntfnsByConfirmHeight[confHeight]
|
|
|
|
if !exists {
|
|
|
|
ntfnSet = make(map[*ConfNtfn]struct{})
|
|
|
|
tcn.ntfnsByConfirmHeight[confHeight] = ntfnSet
|
2018-03-19 22:22:44 +03:00
|
|
|
}
|
2018-09-29 02:30:13 +03:00
|
|
|
ntfnSet[ntfn] = struct{}{}
|
|
|
|
|
|
|
|
// We'll also send an update to the client of how many
|
|
|
|
// confirmations are left for the transaction to be
|
|
|
|
// confirmed.
|
|
|
|
numConfsLeft := confHeight - tcn.currentHeight
|
|
|
|
select {
|
|
|
|
case ntfn.Event.Updates <- numConfsLeft:
|
|
|
|
case <-tcn.quit:
|
|
|
|
return ErrTxConfNotifierExiting
|
|
|
|
}
|
|
|
|
}
|
2017-11-13 22:55:22 +03:00
|
|
|
|
2018-09-29 02:30:13 +03:00
|
|
|
// As a final check, we'll also watch the transaction if it's
|
|
|
|
// still possible for it to get reorged out of the chain.
|
|
|
|
blockHeight := details.BlockHeight
|
|
|
|
reorgSafeHeight := blockHeight + tcn.reorgSafetyLimit
|
|
|
|
if reorgSafeHeight > tcn.currentHeight {
|
|
|
|
txSet, exists := tcn.txsByInitialHeight[blockHeight]
|
|
|
|
if !exists {
|
|
|
|
txSet = make(map[chainhash.Hash]struct{})
|
|
|
|
tcn.txsByInitialHeight[blockHeight] = txSet
|
2018-03-19 22:04:19 +03:00
|
|
|
}
|
2018-09-29 02:30:13 +03:00
|
|
|
txSet[*ntfn.TxID] = struct{}{}
|
2017-11-13 22:55:22 +03:00
|
|
|
}
|
2017-12-05 00:30:33 +03:00
|
|
|
|
|
|
|
return nil
|
2017-11-13 22:55:22 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// ConnectTip handles a new block extending the current chain. This checks each
|
|
|
|
// transaction in the block to see if any watched transactions are included.
|
|
|
|
// Also, if any watched transactions now have the required number of
|
|
|
|
// confirmations as a result of this block being connected, this dispatches
|
|
|
|
// notifications.
|
|
|
|
func (tcn *TxConfNotifier) ConnectTip(blockHash *chainhash.Hash,
|
|
|
|
blockHeight uint32, txns []*btcutil.Tx) error {
|
|
|
|
|
2017-12-05 00:30:33 +03:00
|
|
|
select {
|
|
|
|
case <-tcn.quit:
|
2018-07-27 07:27:27 +03:00
|
|
|
return ErrTxConfNotifierExiting
|
2017-12-05 00:30:33 +03:00
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
2018-07-27 07:31:32 +03:00
|
|
|
tcn.Lock()
|
|
|
|
defer tcn.Unlock()
|
|
|
|
|
2017-11-13 22:55:22 +03:00
|
|
|
if blockHeight != tcn.currentHeight+1 {
|
|
|
|
return fmt.Errorf("Received blocks out of order: "+
|
|
|
|
"current height=%d, new height=%d",
|
|
|
|
tcn.currentHeight, blockHeight)
|
|
|
|
}
|
|
|
|
tcn.currentHeight++
|
2017-11-14 04:32:11 +03:00
|
|
|
tcn.reorgDepth = 0
|
2017-11-13 22:55:22 +03:00
|
|
|
|
2018-03-19 22:22:44 +03:00
|
|
|
// Record any newly confirmed transactions by their confirmed height so
|
|
|
|
// that notifications get dispatched when the transactions reach their
|
|
|
|
// required number of confirmations. We'll also watch these transactions
|
|
|
|
// at the height they were included in the chain so reorgs can be
|
|
|
|
// handled correctly.
|
2017-11-13 22:55:22 +03:00
|
|
|
for _, tx := range txns {
|
|
|
|
txHash := tx.Hash()
|
2018-09-29 23:53:24 +03:00
|
|
|
|
|
|
|
// Check if we have any pending notifications for this txid. If
|
|
|
|
// none are found, we can proceed to the next transaction.
|
2018-08-25 05:29:55 +03:00
|
|
|
confSet, ok := tcn.confNotifications[*txHash]
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2018-09-29 23:53:24 +03:00
|
|
|
Log.Debugf("Block contains txid=%v, constructing details",
|
|
|
|
txHash)
|
|
|
|
|
2018-10-03 23:43:44 +03:00
|
|
|
// If we have any, we'll record its confirmed height so that
|
|
|
|
// notifications get dispatched when the transaction reaches the
|
|
|
|
// clients' desired number of confirmations.
|
2018-09-29 23:53:24 +03:00
|
|
|
details := &TxConfirmation{
|
|
|
|
BlockHash: blockHash,
|
|
|
|
BlockHeight: blockHeight,
|
|
|
|
TxIndex: uint32(tx.Index()),
|
|
|
|
}
|
|
|
|
|
2018-10-03 23:42:41 +03:00
|
|
|
confSet.rescanStatus = rescanComplete
|
2018-09-29 23:53:24 +03:00
|
|
|
confSet.details = details
|
2018-08-25 05:29:55 +03:00
|
|
|
for _, ntfn := range confSet.ntfns {
|
2018-10-03 23:43:44 +03:00
|
|
|
// In the event that this notification was aware that
|
|
|
|
// the transaction was reorged out of the chain, we'll
|
|
|
|
// consume the reorg notification if it hasn't been done
|
|
|
|
// yet already.
|
|
|
|
select {
|
|
|
|
case <-ntfn.Event.NegativeConf:
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
|
|
|
// We'll note this client's required number of
|
|
|
|
// confirmations so that we can notify them when
|
|
|
|
// expected.
|
2017-11-13 22:55:22 +03:00
|
|
|
confHeight := blockHeight + ntfn.NumConfirmations - 1
|
|
|
|
ntfnSet, exists := tcn.ntfnsByConfirmHeight[confHeight]
|
|
|
|
if !exists {
|
|
|
|
ntfnSet = make(map[*ConfNtfn]struct{})
|
|
|
|
tcn.ntfnsByConfirmHeight[confHeight] = ntfnSet
|
|
|
|
}
|
|
|
|
ntfnSet[ntfn] = struct{}{}
|
|
|
|
|
2018-10-03 23:43:44 +03:00
|
|
|
// We'll also note the initial confirmation height in
|
|
|
|
// order to correctly handle dispatching notifications
|
|
|
|
// when the transaction gets reorged out of the chain.
|
2018-03-19 22:04:19 +03:00
|
|
|
txSet, exists := tcn.txsByInitialHeight[blockHeight]
|
|
|
|
if !exists {
|
|
|
|
txSet = make(map[chainhash.Hash]struct{})
|
|
|
|
tcn.txsByInitialHeight[blockHeight] = txSet
|
|
|
|
}
|
|
|
|
txSet[*txHash] = struct{}{}
|
2017-11-13 22:55:22 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-22 22:55:32 +03:00
|
|
|
// In order to update the height hint for all the required transactions
|
|
|
|
// under one database transaction, we'll gather the set of unconfirmed
|
|
|
|
// transactions along with the ones that confirmed at the current
|
|
|
|
// height. To do so, we'll iterate over the confNotifications map, which
|
|
|
|
// contains the transactions we currently have notifications for. Since
|
2018-09-29 23:55:30 +03:00
|
|
|
// this map doesn't tell us whether the transaction has confirmed or
|
2018-05-22 22:55:32 +03:00
|
|
|
// not, we'll need to look at txsByInitialHeight to determine so.
|
|
|
|
var txsToUpdateHints []chainhash.Hash
|
|
|
|
for confirmedTx := range tcn.txsByInitialHeight[tcn.currentHeight] {
|
|
|
|
txsToUpdateHints = append(txsToUpdateHints, confirmedTx)
|
|
|
|
}
|
|
|
|
out:
|
2018-08-25 05:29:55 +03:00
|
|
|
for maybeUnconfirmedTx, confSet := range tcn.confNotifications {
|
2018-09-29 23:55:30 +03:00
|
|
|
// We shouldn't update the confirm hints if we still have a
|
|
|
|
// pending rescan in progress. We'll skip writing any for
|
|
|
|
// notification sets that haven't reached rescanComplete.
|
2018-08-25 05:29:55 +03:00
|
|
|
if confSet.rescanStatus != rescanComplete {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2018-05-22 22:55:32 +03:00
|
|
|
for height, confirmedTxs := range tcn.txsByInitialHeight {
|
|
|
|
// Skip the transactions that confirmed at the new block
|
|
|
|
// height as those have already been added.
|
|
|
|
if height == blockHeight {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the transaction was found within the set of
|
|
|
|
// confirmed transactions at this height, we'll skip it.
|
|
|
|
if _, ok := confirmedTxs[maybeUnconfirmedTx]; ok {
|
|
|
|
continue out
|
|
|
|
}
|
|
|
|
}
|
|
|
|
txsToUpdateHints = append(txsToUpdateHints, maybeUnconfirmedTx)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(txsToUpdateHints) > 0 {
|
|
|
|
err := tcn.hintCache.CommitConfirmHint(
|
|
|
|
tcn.currentHeight, txsToUpdateHints...,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
// The error is not fatal, so we should not return an
|
|
|
|
// error to the caller.
|
|
|
|
Log.Errorf("Unable to update confirm hint to %d for "+
|
|
|
|
"%v: %v", tcn.currentHeight, txsToUpdateHints,
|
|
|
|
err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-19 22:22:44 +03:00
|
|
|
// Next, we'll dispatch an update to all of the notification clients for
|
|
|
|
// our watched transactions with the number of confirmations left at
|
|
|
|
// this new height.
|
|
|
|
for _, txHashes := range tcn.txsByInitialHeight {
|
|
|
|
for txHash := range txHashes {
|
2018-08-25 05:29:55 +03:00
|
|
|
confSet := tcn.confNotifications[txHash]
|
|
|
|
for _, ntfn := range confSet.ntfns {
|
2018-09-29 23:55:30 +03:00
|
|
|
txConfHeight := confSet.details.BlockHeight +
|
2018-03-19 22:22:44 +03:00
|
|
|
ntfn.NumConfirmations - 1
|
|
|
|
numConfsLeft := txConfHeight - blockHeight
|
|
|
|
|
|
|
|
// Since we don't clear notifications until
|
|
|
|
// transactions are no longer under the risk of
|
|
|
|
// being reorganized out of the chain, we'll
|
|
|
|
// skip sending updates for transactions that
|
|
|
|
// have already been confirmed.
|
|
|
|
if int32(numConfsLeft) < 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
select {
|
|
|
|
case ntfn.Event.Updates <- numConfsLeft:
|
|
|
|
case <-tcn.quit:
|
2018-07-27 07:27:27 +03:00
|
|
|
return ErrTxConfNotifierExiting
|
2018-03-19 22:22:44 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Then, we'll dispatch notifications for all the transactions that have
|
|
|
|
// become confirmed at this new block height.
|
2018-10-03 23:43:44 +03:00
|
|
|
for ntfn := range tcn.ntfnsByConfirmHeight[blockHeight] {
|
2018-10-18 04:10:08 +03:00
|
|
|
confSet := tcn.confNotifications[*ntfn.TxID]
|
|
|
|
|
2017-11-13 22:55:22 +03:00
|
|
|
Log.Infof("Dispatching %v conf notification for %v",
|
|
|
|
ntfn.NumConfirmations, ntfn.TxID)
|
2018-07-27 07:27:27 +03:00
|
|
|
|
2017-12-05 00:30:33 +03:00
|
|
|
select {
|
2018-10-18 04:10:08 +03:00
|
|
|
case ntfn.Event.Confirmed <- confSet.details:
|
2017-12-05 00:30:33 +03:00
|
|
|
ntfn.dispatched = true
|
|
|
|
case <-tcn.quit:
|
2018-07-27 07:27:27 +03:00
|
|
|
return ErrTxConfNotifierExiting
|
2017-12-05 00:30:33 +03:00
|
|
|
}
|
2017-11-13 22:55:22 +03:00
|
|
|
}
|
|
|
|
delete(tcn.ntfnsByConfirmHeight, tcn.currentHeight)
|
|
|
|
|
|
|
|
// Clear entries from confNotifications and confTxsByInitialHeight. We
|
2018-03-19 22:04:19 +03:00
|
|
|
// assume that reorgs deeper than the reorg safety limit do not happen,
|
|
|
|
// so we can clear out entries for the block that is now mature.
|
2017-11-13 22:56:25 +03:00
|
|
|
if tcn.currentHeight >= tcn.reorgSafetyLimit {
|
|
|
|
matureBlockHeight := tcn.currentHeight - tcn.reorgSafetyLimit
|
2018-03-19 22:04:19 +03:00
|
|
|
for txHash := range tcn.txsByInitialHeight[matureBlockHeight] {
|
|
|
|
delete(tcn.confNotifications, txHash)
|
2017-11-13 22:56:25 +03:00
|
|
|
}
|
2018-03-19 22:04:19 +03:00
|
|
|
delete(tcn.txsByInitialHeight, matureBlockHeight)
|
2017-11-13 22:55:22 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// DisconnectTip handles the tip of the current chain being disconnected during
|
|
|
|
// a chain reorganization. If any watched transactions were included in this
|
|
|
|
// block, internal structures are updated to ensure a confirmation notification
|
|
|
|
// is not sent unless the transaction is included in the new chain.
|
|
|
|
func (tcn *TxConfNotifier) DisconnectTip(blockHeight uint32) error {
|
2017-12-05 00:30:33 +03:00
|
|
|
select {
|
|
|
|
case <-tcn.quit:
|
2018-07-27 07:31:32 +03:00
|
|
|
return ErrTxConfNotifierExiting
|
2017-12-05 00:30:33 +03:00
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
2018-07-27 07:31:32 +03:00
|
|
|
tcn.Lock()
|
|
|
|
defer tcn.Unlock()
|
|
|
|
|
2017-11-13 22:55:22 +03:00
|
|
|
if blockHeight != tcn.currentHeight {
|
|
|
|
return fmt.Errorf("Received blocks out of order: "+
|
|
|
|
"current height=%d, disconnected height=%d",
|
|
|
|
tcn.currentHeight, blockHeight)
|
|
|
|
}
|
|
|
|
tcn.currentHeight--
|
2017-11-14 04:32:11 +03:00
|
|
|
tcn.reorgDepth++
|
2017-11-13 22:55:22 +03:00
|
|
|
|
2018-08-25 03:58:02 +03:00
|
|
|
// Rewind the height hint for all watched transactions.
|
|
|
|
var txs []chainhash.Hash
|
|
|
|
for tx := range tcn.confNotifications {
|
|
|
|
txs = append(txs, tx)
|
|
|
|
}
|
|
|
|
|
|
|
|
err := tcn.hintCache.CommitConfirmHint(tcn.currentHeight, txs...)
|
|
|
|
if err != nil {
|
|
|
|
Log.Errorf("Unable to update confirm hint to %d for %v: %v",
|
|
|
|
tcn.currentHeight, txs, err)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-03-19 22:22:44 +03:00
|
|
|
// We'll go through all of our watched transactions and attempt to drain
|
|
|
|
// their notification channels to ensure sending notifications to the
|
|
|
|
// clients is always non-blocking.
|
|
|
|
for initialHeight, txHashes := range tcn.txsByInitialHeight {
|
|
|
|
for txHash := range txHashes {
|
2018-10-03 23:47:26 +03:00
|
|
|
// If the transaction has been reorged out of the chain,
|
|
|
|
// we'll make sure to remove the cached confirmation
|
|
|
|
// details to prevent notifying clients with old
|
|
|
|
// information.
|
2018-08-25 05:29:55 +03:00
|
|
|
confSet := tcn.confNotifications[txHash]
|
2018-10-03 23:47:26 +03:00
|
|
|
if initialHeight == blockHeight {
|
|
|
|
confSet.details = nil
|
|
|
|
}
|
|
|
|
|
2018-08-25 05:29:55 +03:00
|
|
|
for _, ntfn := range confSet.ntfns {
|
2018-03-19 22:22:44 +03:00
|
|
|
// First, we'll attempt to drain an update
|
|
|
|
// from each notification to ensure sends to the
|
|
|
|
// Updates channel are always non-blocking.
|
2017-11-14 04:32:11 +03:00
|
|
|
select {
|
2018-03-19 22:22:44 +03:00
|
|
|
case <-ntfn.Event.Updates:
|
2017-12-05 00:30:33 +03:00
|
|
|
case <-tcn.quit:
|
2018-07-27 07:27:27 +03:00
|
|
|
return ErrTxConfNotifierExiting
|
2018-03-19 22:22:44 +03:00
|
|
|
default:
|
2017-11-14 04:32:11 +03:00
|
|
|
}
|
|
|
|
|
2018-03-19 22:22:44 +03:00
|
|
|
// Then, we'll check if the current transaction
|
|
|
|
// was included in the block currently being
|
2018-10-03 23:51:05 +03:00
|
|
|
// disconnected. If it was, we'll need to
|
|
|
|
// dispatch a reorg notification to the client.
|
2018-03-19 22:22:44 +03:00
|
|
|
if initialHeight == blockHeight {
|
2018-10-03 23:51:05 +03:00
|
|
|
err := tcn.dispatchConfReorg(
|
|
|
|
ntfn, blockHeight,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2018-03-19 22:22:44 +03:00
|
|
|
}
|
|
|
|
}
|
2017-11-13 22:55:22 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-03-19 22:04:19 +03:00
|
|
|
|
|
|
|
// Finally, we can remove the transactions we're currently watching that
|
|
|
|
// were included in this block height.
|
|
|
|
delete(tcn.txsByInitialHeight, blockHeight)
|
2017-11-13 22:55:22 +03:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-10-03 23:51:05 +03:00
|
|
|
// dispatchConfReorg dispatches a reorg notification to the client if the
|
|
|
|
// confirmation notification was already delivered.
|
|
|
|
//
|
|
|
|
// NOTE: This must be called with the TxNotifier's lock held.
|
|
|
|
func (tcn *TxConfNotifier) dispatchConfReorg(
|
|
|
|
ntfn *ConfNtfn, heightDisconnected uint32) error {
|
|
|
|
|
|
|
|
// If the transaction's confirmation notification has yet to be
|
|
|
|
// dispatched, we'll need to clear its entry within the
|
|
|
|
// ntfnsByConfirmHeight index to prevent from notifiying the client once
|
|
|
|
// the notifier reaches the confirmation height.
|
|
|
|
if !ntfn.dispatched {
|
|
|
|
confHeight := heightDisconnected + ntfn.NumConfirmations - 1
|
|
|
|
ntfnSet, exists := tcn.ntfnsByConfirmHeight[confHeight]
|
|
|
|
if exists {
|
|
|
|
delete(ntfnSet, ntfn)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise, the entry within the ntfnsByConfirmHeight has already been
|
|
|
|
// deleted, so we'll attempt to drain the confirmation notification to
|
|
|
|
// ensure sends to the Confirmed channel are always non-blocking.
|
|
|
|
select {
|
|
|
|
case <-ntfn.Event.Confirmed:
|
|
|
|
case <-tcn.quit:
|
|
|
|
return ErrTxConfNotifierExiting
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
|
|
|
ntfn.dispatched = false
|
|
|
|
|
|
|
|
// Send a negative confirmation notification to the client indicating
|
|
|
|
// how many blocks have been disconnected successively.
|
|
|
|
select {
|
|
|
|
case ntfn.Event.NegativeConf <- int32(tcn.reorgDepth):
|
|
|
|
case <-tcn.quit:
|
|
|
|
return ErrTxConfNotifierExiting
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-11-13 22:55:22 +03:00
|
|
|
// TearDown is to be called when the owner of the TxConfNotifier is exiting.
|
|
|
|
// This closes the event channels of all registered notifications that have
|
|
|
|
// not been dispatched yet.
|
|
|
|
func (tcn *TxConfNotifier) TearDown() {
|
2018-07-27 07:31:32 +03:00
|
|
|
tcn.Lock()
|
|
|
|
defer tcn.Unlock()
|
|
|
|
|
2017-12-05 00:30:33 +03:00
|
|
|
close(tcn.quit)
|
|
|
|
|
2018-08-25 05:29:55 +03:00
|
|
|
for _, confSet := range tcn.confNotifications {
|
|
|
|
for _, ntfn := range confSet.ntfns {
|
2017-11-13 22:55:22 +03:00
|
|
|
if ntfn.dispatched {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
close(ntfn.Event.Confirmed)
|
2018-03-19 21:48:44 +03:00
|
|
|
close(ntfn.Event.Updates)
|
2017-11-13 22:55:22 +03:00
|
|
|
close(ntfn.Event.NegativeConf)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|