mirror of
https://github.com/lightninglabs/pool.git
synced 2026-08-13 12:33:04 +02:00
339 lines
8.4 KiB
Go
339 lines
8.4 KiB
Go
package watcher
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/lightninglabs/lndclient"
|
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
// CtrlConfig contains all of the Controller's dependencies in order to carry out its
|
|
// duties.
|
|
type CtrlConfig struct {
|
|
// ChainNotifier is responsible for requesting confirmation and spend
|
|
// notifications for accounts.
|
|
ChainNotifier lndclient.ChainNotifierClient
|
|
|
|
// Handlers define the handler to be used after receiving every event.
|
|
Handlers EventHandler
|
|
}
|
|
|
|
// controller implements the Controller interface.
|
|
type controller struct {
|
|
started sync.Once
|
|
stopped sync.Once
|
|
|
|
cfg *CtrlConfig
|
|
|
|
watcher ExpiryWatcher
|
|
|
|
wg sync.WaitGroup
|
|
quit chan struct{}
|
|
ctxCancels []func()
|
|
|
|
cancelMtx sync.Mutex
|
|
spendCancels map[[33]byte]func()
|
|
confCancels map[[33]byte]func()
|
|
}
|
|
|
|
// Compile time assertion that controller implements the Controller interface?
|
|
var _ Controller = (*controller)(nil)
|
|
|
|
// NewController returns an internal struct type that implements the
|
|
// Controller interface.
|
|
func NewController(cfg *CtrlConfig) *controller { // nolint:golint
|
|
watcher := NewExpiryWatcher(cfg.Handlers)
|
|
return &controller{
|
|
cfg: cfg,
|
|
watcher: watcher,
|
|
quit: make(chan struct{}),
|
|
spendCancels: make(map[[33]byte]func()),
|
|
confCancels: make(map[[33]byte]func()),
|
|
}
|
|
}
|
|
|
|
// Start allows the Watcher to begin accepting watch requests.
|
|
func (c *controller) Start() error {
|
|
var err error
|
|
c.started.Do(func() {
|
|
err = c.start()
|
|
})
|
|
return err
|
|
}
|
|
|
|
// start allows the Watcher to begin accepting watch requests.
|
|
func (c *controller) start() error {
|
|
ctxc, cancel := context.WithCancel(context.Background())
|
|
blockChan, errChan, err := c.cfg.ChainNotifier.RegisterBlockEpochNtfn(
|
|
ctxc,
|
|
)
|
|
if err != nil {
|
|
cancel()
|
|
return err
|
|
}
|
|
c.ctxCancels = append(c.ctxCancels, cancel)
|
|
|
|
c.wg.Add(1)
|
|
go c.expiryHandler(blockChan, errChan)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop safely stops any ongoing requests within the Watcher.
|
|
func (c *controller) Stop() {
|
|
c.stopped.Do(func() {
|
|
close(c.quit)
|
|
c.wg.Wait()
|
|
|
|
for _, cancel := range c.ctxCancels {
|
|
cancel()
|
|
}
|
|
|
|
c.cancelMtx.Lock()
|
|
for _, cancel := range c.spendCancels {
|
|
cancel()
|
|
}
|
|
for _, cancel := range c.confCancels {
|
|
cancel()
|
|
}
|
|
c.cancelMtx.Unlock()
|
|
})
|
|
}
|
|
|
|
// expiryHandler receives block notifications to determine when accounts expire.
|
|
//
|
|
// NOTE: This must be run as a goroutine.
|
|
func (c *controller) expiryHandler(blockChan chan int32, errChan chan error) {
|
|
defer c.wg.Done()
|
|
|
|
// Wait for the initial block notification to be received before we
|
|
// begin handling requests.
|
|
select {
|
|
case newBlock := <-blockChan:
|
|
c.watcher.NewBlock(uint32(newBlock))
|
|
case err := <-errChan:
|
|
log.Errorf("Unable to receive initial block notification: %v",
|
|
err)
|
|
case <-c.quit:
|
|
return
|
|
}
|
|
|
|
for {
|
|
select {
|
|
// A new block notification has arrived, update our known
|
|
// height and notify any newly expired accounts.
|
|
case newBlock := <-blockChan:
|
|
c.watcher.NewBlock(uint32(newBlock))
|
|
|
|
// An error occurred while being sent a block notification.
|
|
case err := <-errChan:
|
|
log.Errorf("Unable to receive block notification: %v",
|
|
err)
|
|
|
|
case <-c.quit:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// WatchAccountConf watches a new account on-chain for its confirmation. Only
|
|
// one conf watcher per account can be used at any time.
|
|
//
|
|
// NOTE: If there is a previous conf watcher for the given account that has not
|
|
// finished yet, it will be canceled!
|
|
func (c *controller) WatchAccountConf(traderKey *btcec.PublicKey,
|
|
txHash chainhash.Hash, script []byte, numConfs, heightHint uint32) error {
|
|
|
|
c.cancelMtx.Lock()
|
|
defer c.cancelMtx.Unlock()
|
|
|
|
var traderKeyRaw [33]byte
|
|
copy(traderKeyRaw[:], traderKey.SerializeCompressed())
|
|
|
|
// Cancel a previous conf watcher if one still exists.
|
|
cancel, ok := c.confCancels[traderKeyRaw]
|
|
if ok {
|
|
cancel()
|
|
}
|
|
|
|
ctxc, cancel := context.WithCancel(context.Background())
|
|
confChan, errChan, err := c.cfg.ChainNotifier.RegisterConfirmationsNtfn(
|
|
ctxc, &txHash, script, int32(numConfs), int32(heightHint),
|
|
)
|
|
if err != nil {
|
|
cancel()
|
|
return err
|
|
}
|
|
c.confCancels[traderKeyRaw] = cancel
|
|
|
|
c.wg.Add(1)
|
|
go c.waitForAccountConf(traderKey, traderKeyRaw, confChan, errChan)
|
|
|
|
return nil
|
|
}
|
|
|
|
// waitForAccountConf waits for an account's confirmation and takes the
|
|
// necessary steps once confirmed.
|
|
//
|
|
// NOTE: This method must be run as a goroutine.
|
|
func (c *controller) waitForAccountConf(traderKey *btcec.PublicKey,
|
|
traderKeyRaw [33]byte, confChan chan *chainntnfs.TxConfirmation,
|
|
errChan chan error) {
|
|
|
|
defer func() {
|
|
c.wg.Done()
|
|
|
|
c.cancelMtx.Lock()
|
|
delete(c.confCancels, traderKeyRaw)
|
|
c.cancelMtx.Unlock()
|
|
}()
|
|
|
|
select {
|
|
case conf := <-confChan:
|
|
err := c.cfg.Handlers.HandleAccountConf(traderKey, conf)
|
|
if err != nil {
|
|
log.Errorf("Unable to handle confirmation for account "+
|
|
"%x: %v", traderKey.SerializeCompressed(), err)
|
|
}
|
|
|
|
case err := <-errChan:
|
|
if err != nil {
|
|
// Ignore context canceled error due to possible manual
|
|
// cancellation.
|
|
s, ok := status.FromError(err)
|
|
if ok && s.Code() == codes.Canceled {
|
|
return
|
|
}
|
|
|
|
log.Errorf("Unable to determine confirmation for "+
|
|
"account %x: %v",
|
|
traderKey.SerializeCompressed(), err)
|
|
}
|
|
|
|
case <-c.quit:
|
|
return
|
|
}
|
|
}
|
|
|
|
// WatchAccountSpend watches for the spend of an account. Only one spend watcher
|
|
// per account can be used at any time.
|
|
//
|
|
// NOTE: If there is a previous spend watcher for the given account that has not
|
|
// finished yet, it will be canceled!
|
|
func (c *controller) WatchAccountSpend(traderKey *btcec.PublicKey,
|
|
accountPoint wire.OutPoint, script []byte, heightHint uint32) error {
|
|
|
|
c.cancelMtx.Lock()
|
|
defer c.cancelMtx.Unlock()
|
|
|
|
var traderKeyRaw [33]byte
|
|
copy(traderKeyRaw[:], traderKey.SerializeCompressed())
|
|
|
|
// Cancel a previous spend watcher if one still exists.
|
|
cancel, ok := c.spendCancels[traderKeyRaw]
|
|
if ok {
|
|
cancel()
|
|
}
|
|
|
|
ctxc, cancel := context.WithCancel(context.Background())
|
|
spendChan, errChan, err := c.cfg.ChainNotifier.RegisterSpendNtfn(
|
|
ctxc, &accountPoint, script, int32(heightHint),
|
|
)
|
|
if err != nil {
|
|
cancel()
|
|
return err
|
|
}
|
|
c.spendCancels[traderKeyRaw] = cancel
|
|
|
|
c.wg.Add(1)
|
|
go c.waitForAccountSpend(traderKey, traderKeyRaw, spendChan, errChan)
|
|
|
|
return nil
|
|
}
|
|
|
|
// waitForAccountSpend waits for an account's spend and takes the necessary
|
|
// steps once spent.
|
|
//
|
|
// NOTE: This method must be run as a goroutine.
|
|
func (c *controller) waitForAccountSpend(traderKey *btcec.PublicKey,
|
|
traderKeyRaw [33]byte, spendChan chan *chainntnfs.SpendDetail,
|
|
errChan chan error) {
|
|
|
|
defer func() {
|
|
c.wg.Done()
|
|
|
|
c.cancelMtx.Lock()
|
|
delete(c.spendCancels, traderKeyRaw)
|
|
c.cancelMtx.Unlock()
|
|
}()
|
|
|
|
select {
|
|
case spend := <-spendChan:
|
|
err := c.cfg.Handlers.HandleAccountSpend(traderKey, spend)
|
|
if err != nil {
|
|
log.Errorf("Unable to handle spend for account %x: %v",
|
|
traderKey.SerializeCompressed(), err)
|
|
}
|
|
|
|
case err := <-errChan:
|
|
if err != nil {
|
|
// Ignore context canceled error due to possible manual
|
|
// cancellation.
|
|
s, ok := status.FromError(err)
|
|
if ok && s.Code() == codes.Canceled {
|
|
return
|
|
}
|
|
|
|
log.Errorf("Unable to determine spend for account %x: "+
|
|
"%v", traderKey.SerializeCompressed(), err)
|
|
}
|
|
|
|
case <-c.quit:
|
|
return
|
|
}
|
|
}
|
|
|
|
// WatchAccountExpiration watches for the expiration of an account on-chain.
|
|
// Successive calls for the same account will cancel any previous expiration
|
|
// watch requests and the new expiration will be tracked instead.
|
|
func (c *controller) WatchAccountExpiration(traderKey *btcec.PublicKey,
|
|
expiry uint32) {
|
|
|
|
c.watcher.AddAccountExpiration(traderKey, expiry)
|
|
}
|
|
|
|
// CancelAccountSpend cancels the spend watcher of the given account, if one is
|
|
// active.
|
|
func (c *controller) CancelAccountSpend(traderKey *btcec.PublicKey) {
|
|
c.cancelMtx.Lock()
|
|
defer c.cancelMtx.Unlock()
|
|
|
|
var traderKeyRaw [33]byte
|
|
copy(traderKeyRaw[:], traderKey.SerializeCompressed())
|
|
|
|
cancel, ok := c.spendCancels[traderKeyRaw]
|
|
if ok {
|
|
cancel()
|
|
}
|
|
}
|
|
|
|
// CancelAccountConf cancels the conf watcher of the given account, if one is
|
|
// active.
|
|
func (c *controller) CancelAccountConf(traderKey *btcec.PublicKey) {
|
|
c.cancelMtx.Lock()
|
|
defer c.cancelMtx.Unlock()
|
|
|
|
var traderKeyRaw [33]byte
|
|
copy(traderKeyRaw[:], traderKey.SerializeCompressed())
|
|
|
|
cancel, ok := c.confCancels[traderKeyRaw]
|
|
if ok {
|
|
cancel()
|
|
}
|
|
}
|