2023-08-24 23:36:53 +00:00
|
|
|
package reservation
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
|
|
"github.com/lightninglabs/loop/fsm"
|
|
|
|
looprpc "github.com/lightninglabs/loop/swapserverrpc"
|
2023-08-24 23:54:18 +00:00
|
|
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
2023-08-24 23:36:53 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// InitReservationContext contains the request parameters for a reservation.
|
|
|
|
type InitReservationContext struct {
|
|
|
|
reservationID ID
|
|
|
|
serverPubkey *btcec.PublicKey
|
|
|
|
value btcutil.Amount
|
|
|
|
expiry uint32
|
|
|
|
heightHint uint32
|
|
|
|
}
|
|
|
|
|
|
|
|
// InitAction is the action that is executed when the reservation state machine
|
|
|
|
// is initialized. It creates the reservation in the database and dispatches the
|
|
|
|
// payment to the server.
|
2023-08-24 23:54:18 +00:00
|
|
|
func (f *FSM) InitAction(eventCtx fsm.EventContext) fsm.EventType {
|
2023-08-24 23:36:53 +00:00
|
|
|
// Check if the context is of the correct type.
|
|
|
|
reservationRequest, ok := eventCtx.(*InitReservationContext)
|
|
|
|
if !ok {
|
2023-08-24 23:54:18 +00:00
|
|
|
return f.HandleError(fsm.ErrInvalidContextType)
|
2023-08-24 23:36:53 +00:00
|
|
|
}
|
|
|
|
|
2023-08-24 23:54:18 +00:00
|
|
|
keyRes, err := f.cfg.Wallet.DeriveNextKey(
|
|
|
|
f.ctx, KeyFamily,
|
2023-08-24 23:36:53 +00:00
|
|
|
)
|
|
|
|
if err != nil {
|
2023-08-24 23:54:18 +00:00
|
|
|
return f.HandleError(err)
|
2023-08-24 23:36:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Send the client reservation details to the server.
|
|
|
|
log.Debugf("Dispatching reservation to server: %x",
|
|
|
|
reservationRequest.reservationID)
|
|
|
|
|
|
|
|
request := &looprpc.ServerOpenReservationRequest{
|
|
|
|
ReservationId: reservationRequest.reservationID[:],
|
|
|
|
ClientKey: keyRes.PubKey.SerializeCompressed(),
|
|
|
|
}
|
|
|
|
|
2023-08-24 23:54:18 +00:00
|
|
|
_, err = f.cfg.ReservationClient.OpenReservation(f.ctx, request)
|
2023-08-24 23:36:53 +00:00
|
|
|
if err != nil {
|
2023-08-24 23:54:18 +00:00
|
|
|
return f.HandleError(err)
|
2023-08-24 23:36:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
reservation, err := NewReservation(
|
|
|
|
reservationRequest.reservationID,
|
|
|
|
reservationRequest.serverPubkey,
|
|
|
|
keyRes.PubKey,
|
|
|
|
reservationRequest.value,
|
|
|
|
reservationRequest.expiry,
|
|
|
|
reservationRequest.heightHint,
|
|
|
|
keyRes.KeyLocator,
|
|
|
|
)
|
|
|
|
if err != nil {
|
2023-08-24 23:54:18 +00:00
|
|
|
return f.HandleError(err)
|
2023-08-24 23:36:53 +00:00
|
|
|
}
|
|
|
|
|
2023-08-24 23:54:18 +00:00
|
|
|
f.reservation = reservation
|
2023-08-24 23:36:53 +00:00
|
|
|
|
|
|
|
// Create the reservation in the database.
|
2023-08-24 23:54:18 +00:00
|
|
|
err = f.cfg.Store.CreateReservation(f.ctx, reservation)
|
2023-08-24 23:36:53 +00:00
|
|
|
if err != nil {
|
2023-08-24 23:54:18 +00:00
|
|
|
return f.HandleError(err)
|
2023-08-24 23:36:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return OnBroadcast
|
|
|
|
}
|
|
|
|
|
|
|
|
// SubscribeToConfirmationAction is the action that is executed when the
|
|
|
|
// reservation is waiting for confirmation. It subscribes to the confirmation
|
|
|
|
// of the reservation transaction.
|
2023-08-24 23:54:18 +00:00
|
|
|
func (f *FSM) SubscribeToConfirmationAction(_ fsm.EventContext) fsm.EventType {
|
|
|
|
pkscript, err := f.reservation.GetPkScript()
|
2023-08-24 23:36:53 +00:00
|
|
|
if err != nil {
|
2023-08-24 23:54:18 +00:00
|
|
|
return f.HandleError(err)
|
2023-08-24 23:36:53 +00:00
|
|
|
}
|
|
|
|
|
2023-08-24 23:54:18 +00:00
|
|
|
callCtx, cancel := context.WithCancel(f.ctx)
|
2023-08-24 23:36:53 +00:00
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
// Subscribe to the confirmation of the reservation transaction.
|
|
|
|
log.Debugf("Subscribing to conf for reservation: %x pkscript: %x, "+
|
2023-08-24 23:54:18 +00:00
|
|
|
"initiation height: %v", f.reservation.ID, pkscript,
|
|
|
|
f.reservation.InitiationHeight)
|
2023-08-24 23:36:53 +00:00
|
|
|
|
2023-08-24 23:54:18 +00:00
|
|
|
confChan, errConfChan, err := f.cfg.ChainNotifier.RegisterConfirmationsNtfn(
|
2023-08-24 23:36:53 +00:00
|
|
|
callCtx, nil, pkscript, DefaultConfTarget,
|
2023-08-24 23:54:18 +00:00
|
|
|
f.reservation.InitiationHeight,
|
2023-08-24 23:36:53 +00:00
|
|
|
)
|
|
|
|
if err != nil {
|
2023-08-24 23:54:18 +00:00
|
|
|
f.Errorf("unable to subscribe to conf notification: %v", err)
|
|
|
|
return f.HandleError(err)
|
2023-08-24 23:36:53 +00:00
|
|
|
}
|
|
|
|
|
2023-08-24 23:54:18 +00:00
|
|
|
blockChan, errBlockChan, err := f.cfg.ChainNotifier.RegisterBlockEpochNtfn(
|
2023-08-24 23:36:53 +00:00
|
|
|
callCtx,
|
|
|
|
)
|
|
|
|
if err != nil {
|
2023-08-24 23:54:18 +00:00
|
|
|
f.Errorf("unable to subscribe to block notifications: %v", err)
|
|
|
|
return f.HandleError(err)
|
2023-08-24 23:36:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// We'll now wait for the confirmation of the reservation transaction.
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case err := <-errConfChan:
|
2023-08-24 23:54:18 +00:00
|
|
|
f.Errorf("conf subscription error: %v", err)
|
|
|
|
return f.HandleError(err)
|
2023-08-24 23:36:53 +00:00
|
|
|
|
|
|
|
case err := <-errBlockChan:
|
2023-08-24 23:54:18 +00:00
|
|
|
f.Errorf("block subscription error: %v", err)
|
|
|
|
return f.HandleError(err)
|
2023-08-24 23:36:53 +00:00
|
|
|
|
|
|
|
case confInfo := <-confChan:
|
2024-02-05 15:13:46 +00:00
|
|
|
f.Debugf("confirmed in tx: %v", confInfo.Tx)
|
2023-08-24 23:54:18 +00:00
|
|
|
outpoint, err := f.reservation.findReservationOutput(
|
2023-08-24 23:36:53 +00:00
|
|
|
confInfo.Tx,
|
|
|
|
)
|
|
|
|
if err != nil {
|
2023-08-24 23:54:18 +00:00
|
|
|
return f.HandleError(err)
|
2023-08-24 23:36:53 +00:00
|
|
|
}
|
|
|
|
|
2023-08-24 23:54:18 +00:00
|
|
|
f.reservation.ConfirmationHeight = confInfo.BlockHeight
|
|
|
|
f.reservation.Outpoint = outpoint
|
2023-08-24 23:36:53 +00:00
|
|
|
|
|
|
|
return OnConfirmed
|
|
|
|
|
|
|
|
case block := <-blockChan:
|
2023-08-24 23:54:18 +00:00
|
|
|
f.Debugf("block received: %v expiry: %v", block,
|
|
|
|
f.reservation.Expiry)
|
2023-08-24 23:36:53 +00:00
|
|
|
|
2023-08-24 23:54:18 +00:00
|
|
|
if uint32(block) >= f.reservation.Expiry {
|
2023-08-24 23:36:53 +00:00
|
|
|
return OnTimedOut
|
|
|
|
}
|
|
|
|
|
2023-08-24 23:54:18 +00:00
|
|
|
case <-f.ctx.Done():
|
2023-08-24 23:36:53 +00:00
|
|
|
return fsm.NoOp
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-24 23:54:18 +00:00
|
|
|
// AsyncWaitForExpiredOrSweptAction waits for the reservation to be either
|
|
|
|
// expired or swept. This is non-blocking and can be used to wait for the
|
|
|
|
// reservation to expire while expecting other events.
|
|
|
|
func (f *FSM) AsyncWaitForExpiredOrSweptAction(_ fsm.EventContext,
|
|
|
|
) fsm.EventType {
|
|
|
|
|
|
|
|
notifCtx, cancel := context.WithCancel(f.ctx)
|
|
|
|
|
|
|
|
blockHeightChan, errEpochChan, err := f.cfg.ChainNotifier.
|
|
|
|
RegisterBlockEpochNtfn(notifCtx)
|
2023-08-24 23:36:53 +00:00
|
|
|
if err != nil {
|
2023-08-24 23:54:18 +00:00
|
|
|
cancel()
|
|
|
|
return f.HandleError(err)
|
2023-08-24 23:36:53 +00:00
|
|
|
}
|
|
|
|
|
2023-08-24 23:54:18 +00:00
|
|
|
pkScript, err := f.reservation.GetPkScript()
|
|
|
|
if err != nil {
|
|
|
|
cancel()
|
|
|
|
return f.HandleError(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
spendChan, errSpendChan, err := f.cfg.ChainNotifier.RegisterSpendNtfn(
|
|
|
|
notifCtx, f.reservation.Outpoint, pkScript,
|
|
|
|
f.reservation.InitiationHeight,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
cancel()
|
|
|
|
return f.HandleError(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
defer cancel()
|
|
|
|
op, err := f.handleSubcriptions(
|
|
|
|
notifCtx, blockHeightChan, spendChan, errEpochChan,
|
|
|
|
errSpendChan,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
f.handleAsyncError(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if op == fsm.NoOp {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
err = f.SendEvent(op, nil)
|
|
|
|
if err != nil {
|
|
|
|
f.Errorf("Error sending %s event: %v", op, err)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
return fsm.NoOp
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *FSM) handleSubcriptions(ctx context.Context,
|
|
|
|
blockHeightChan <-chan int32, spendChan <-chan *chainntnfs.SpendDetail,
|
|
|
|
errEpochChan <-chan error, errSpendChan <-chan error,
|
|
|
|
) (fsm.EventType, error) {
|
|
|
|
|
2023-08-24 23:36:53 +00:00
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case err := <-errEpochChan:
|
2023-08-24 23:54:18 +00:00
|
|
|
return fsm.OnError, err
|
|
|
|
|
|
|
|
case err := <-errSpendChan:
|
|
|
|
return fsm.OnError, err
|
2023-08-24 23:36:53 +00:00
|
|
|
|
|
|
|
case blockHeight := <-blockHeightChan:
|
2023-08-24 23:54:18 +00:00
|
|
|
expired := blockHeight >= int32(f.reservation.Expiry)
|
2023-08-24 23:36:53 +00:00
|
|
|
|
2023-08-24 23:54:18 +00:00
|
|
|
if expired {
|
|
|
|
f.Debugf("Reservation expired")
|
|
|
|
return OnTimedOut, nil
|
2023-08-24 23:36:53 +00:00
|
|
|
}
|
|
|
|
|
2023-08-24 23:54:18 +00:00
|
|
|
case <-spendChan:
|
|
|
|
return OnSpent, nil
|
|
|
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
return fsm.NoOp, nil
|
2023-08-24 23:36:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-08-24 23:54:18 +00:00
|
|
|
|
|
|
|
func (f *FSM) handleAsyncError(err error) {
|
|
|
|
f.LastActionError = err
|
|
|
|
f.Errorf("Error on async action: %v", err)
|
|
|
|
err2 := f.SendEvent(fsm.OnError, err)
|
|
|
|
if err2 != nil {
|
|
|
|
f.Errorf("Error sending event: %v", err2)
|
|
|
|
}
|
|
|
|
}
|