pool/account/watcher/controller.go
2022-08-03 20:33:06 +02:00

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()
}
}