mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
Refresh the chain height when a swap resumes, verify both the reservation and HTLC windows, and select the HTLC path when either remaining window is too short.
736 lines
20 KiB
Go
736 lines
20 KiB
Go
package instantout
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/btcsuite/btcd/txscript"
|
|
"github.com/lightninglabs/lndclient"
|
|
"github.com/lightninglabs/loop/fsm"
|
|
"github.com/lightninglabs/loop/instantout/reservation"
|
|
"github.com/lightninglabs/loop/loopdb"
|
|
"github.com/lightninglabs/loop/swap"
|
|
"github.com/lightninglabs/loop/swapserverrpc"
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
|
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
|
)
|
|
|
|
const (
|
|
// Define route independent max routing fees. We have currently no way
|
|
// to get a reliable estimate of the routing fees. Best we can do is
|
|
// the minimum routing fees, which is not very indicative.
|
|
maxRoutingFeeBase = btcutil.Amount(10)
|
|
|
|
maxRoutingFeeRate = int64(20000)
|
|
|
|
// urgentConfTarget is the target number of blocks for the htlc to be
|
|
// confirmed quickly.
|
|
urgentConfTarget = int32(3)
|
|
|
|
// normalConfTarget is the target number of blocks for the sweepless
|
|
// sweep to be confirmed.
|
|
normalConfTarget = int32(6)
|
|
|
|
// defaultMaxParts is the default maximum number of parts for the swap.
|
|
defaultMaxParts = uint32(5)
|
|
|
|
// defaultSendpaymentTimeout is the default timeout for the swap invoice.
|
|
defaultSendpaymentTimeout = time.Minute * 5
|
|
|
|
// defaultPollPaymentTime is the default time to poll the server for the
|
|
// payment status.
|
|
defaultPollPaymentTime = time.Second * 15
|
|
|
|
// htlcExpiryDelta is the delta in blocks we require between the htlc
|
|
// expiry and reservation expiry.
|
|
htlcExpiryDelta = int32(40)
|
|
|
|
// htlcRecoverySafetyDelta leaves one urgent confirmation target for
|
|
// the HTLC and another for its preimage sweep after recovery.
|
|
htlcRecoverySafetyDelta = 2 * urgentConfTarget
|
|
)
|
|
|
|
// InitInstantOutCtx contains the context for the InitInstantOutAction.
|
|
type InitInstantOutCtx struct {
|
|
cltvExpiry int32
|
|
reservations []reservation.ID
|
|
initationHeight int32
|
|
outgoingChanSet loopdb.ChannelSet
|
|
protocolVersion ProtocolVersion
|
|
sweepAddress btcutil.Address
|
|
}
|
|
|
|
// RecoverInstantOutCtx marks an action as being resumed after restart.
|
|
type RecoverInstantOutCtx struct{}
|
|
|
|
// InitInstantOutAction is the first action that is executed when the instant
|
|
// out FSM is started. It will send the instant out request to the server.
|
|
func (f *FSM) InitInstantOutAction(ctx context.Context,
|
|
eventCtx fsm.EventContext) fsm.EventType {
|
|
|
|
initCtx, ok := eventCtx.(*InitInstantOutCtx)
|
|
if !ok {
|
|
return f.HandleError(fsm.ErrInvalidContextType)
|
|
}
|
|
|
|
if len(initCtx.reservations) == 0 {
|
|
return f.HandleError(fmt.Errorf("no reservations provided"))
|
|
}
|
|
|
|
var (
|
|
reservationAmt uint64
|
|
reservationIds = make([][]byte, 0, len(initCtx.reservations))
|
|
reservations = make(
|
|
[]*reservation.Reservation, 0, len(initCtx.reservations),
|
|
)
|
|
)
|
|
|
|
// The requested amount needs to be full reservation amounts.
|
|
for _, reservationId := range initCtx.reservations {
|
|
resId := reservationId
|
|
res, err := f.cfg.ReservationManager.GetReservation(ctx, resId)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
// Check if the reservation is locked.
|
|
if res.State == reservation.Locked {
|
|
return f.HandleError(fmt.Errorf("reservation %v is "+
|
|
"locked", reservationId))
|
|
}
|
|
|
|
reservationAmt += uint64(res.Value)
|
|
reservationIds = append(reservationIds, resId[:])
|
|
reservations = append(reservations, res)
|
|
|
|
// Check that the reservation expiry is larger than the cltv
|
|
// expiry of the swap, with an additional delta to allow for
|
|
// preimage reveal.
|
|
if int32(res.Expiry) < initCtx.cltvExpiry+htlcExpiryDelta {
|
|
return f.HandleError(fmt.Errorf("reservation %x has "+
|
|
"expiry %v which is less than the swap expiry %v",
|
|
resId, res.Expiry, initCtx.cltvExpiry+htlcExpiryDelta))
|
|
}
|
|
}
|
|
|
|
// Create the preimage for the swap.
|
|
var preimage lntypes.Preimage
|
|
if _, err := rand.Read(preimage[:]); err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
// Create the keys for the swap.
|
|
keyRes, err := f.cfg.Wallet.DeriveNextKey(ctx, KeyFamily)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
swapHash := preimage.Hash()
|
|
|
|
// Create a high fee rate so that the htlc will be confirmed quickly.
|
|
feeRate, err := f.cfg.Wallet.EstimateFeeRate(ctx, urgentConfTarget)
|
|
if err != nil {
|
|
f.Infof("error estimating fee rate: %v", err)
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
// Send the instantout request to the server.
|
|
instantOutResponse, err := f.cfg.InstantOutClient.RequestInstantLoopOut(
|
|
ctx,
|
|
&swapserverrpc.InstantLoopOutRequest{
|
|
ReceiverKey: keyRes.PubKey.SerializeCompressed(),
|
|
SwapHash: swapHash[:],
|
|
Expiry: initCtx.cltvExpiry,
|
|
HtlcFeeRate: uint64(feeRate),
|
|
ReservationIds: reservationIds,
|
|
ProtocolVersion: CurrentRpcProtocolVersion(),
|
|
},
|
|
)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
// Decode the invoice to check if the hash is valid.
|
|
payReq, err := f.cfg.LndClient.DecodePaymentRequest(
|
|
ctx, instantOutResponse.SwapInvoice,
|
|
)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
if swapHash != payReq.Hash {
|
|
return f.HandleError(fmt.Errorf("invalid swap invoice hash: "+
|
|
"expected %x got %x", preimage.Hash(), payReq.Hash))
|
|
}
|
|
serverPubkey, err := btcec.ParsePubKey(instantOutResponse.SenderKey)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
// Create the address that we'll send the funds to.
|
|
sweepAddress := initCtx.sweepAddress
|
|
if sweepAddress == nil {
|
|
sweepAddress, err = f.cfg.Wallet.NextAddr(
|
|
ctx, lnwallet.DefaultAccountName,
|
|
walletrpc.AddressType_TAPROOT_PUBKEY, false,
|
|
)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
}
|
|
|
|
// Now we can create the instant out.
|
|
instantOut := &InstantOut{
|
|
SwapHash: swapHash,
|
|
swapPreimage: preimage,
|
|
protocolVersion: ProtocolVersionFullReservation,
|
|
initiationHeight: initCtx.initationHeight,
|
|
outgoingChanSet: initCtx.outgoingChanSet,
|
|
CltvExpiry: initCtx.cltvExpiry,
|
|
clientPubkey: keyRes.PubKey,
|
|
serverPubkey: serverPubkey,
|
|
Value: btcutil.Amount(reservationAmt),
|
|
htlcFeeRate: feeRate,
|
|
swapInvoice: instantOutResponse.SwapInvoice,
|
|
Reservations: reservations,
|
|
keyLocator: keyRes.KeyLocator,
|
|
sweepAddress: sweepAddress,
|
|
}
|
|
|
|
err = f.cfg.Store.CreateInstantLoopOut(ctx, instantOut)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
f.InstantOut = instantOut
|
|
|
|
return OnInit
|
|
}
|
|
|
|
// PollPaymentAcceptedAction locks the reservations, sends the payment to the
|
|
// server and polls the server for the payment status.
|
|
func (f *FSM) PollPaymentAcceptedAction(ctx context.Context,
|
|
_ fsm.EventContext) fsm.EventType {
|
|
|
|
// Now that we're doing the swap, we first lock the reservations
|
|
// so that they can't be used for other swaps.
|
|
for _, reservation := range f.InstantOut.Reservations {
|
|
err := f.cfg.ReservationManager.LockReservation(
|
|
ctx, reservation.ID,
|
|
)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(ctx, err)
|
|
}
|
|
}
|
|
|
|
// Now we send the payment to the server.
|
|
payChan, paymentErrChan, err := f.cfg.RouterClient.SendPayment(
|
|
ctx,
|
|
lndclient.SendPaymentRequest{
|
|
Invoice: f.InstantOut.swapInvoice,
|
|
Timeout: defaultSendpaymentTimeout,
|
|
MaxParts: defaultMaxParts,
|
|
MaxFee: getMaxRoutingFee(f.InstantOut.Value),
|
|
},
|
|
)
|
|
if err != nil {
|
|
f.Errorf("error sending payment: %v", err)
|
|
return f.handleErrorAndUnlockReservations(ctx, err)
|
|
}
|
|
|
|
// We'll continuously poll the server for the payment status.
|
|
pollPaymentTries := 0
|
|
|
|
// We want to poll quickly the first time.
|
|
timer := time.NewTimer(time.Second)
|
|
for {
|
|
select {
|
|
case payRes := <-payChan:
|
|
f.Debugf("payment result: %v", payRes)
|
|
if payRes.State == lnrpc.Payment_FAILED {
|
|
return f.handleErrorAndUnlockReservations(
|
|
ctx, fmt.Errorf("payment failed: %v",
|
|
payRes.FailureReason),
|
|
)
|
|
}
|
|
case err := <-paymentErrChan:
|
|
f.Errorf("error sending payment: %v", err)
|
|
return f.handleErrorAndUnlockReservations(ctx, err)
|
|
|
|
case <-ctx.Done():
|
|
return f.handleErrorAndUnlockReservations(ctx, nil)
|
|
|
|
case <-timer.C:
|
|
res, err := f.cfg.InstantOutClient.PollPaymentAccepted(
|
|
ctx,
|
|
&swapserverrpc.PollPaymentAcceptedRequest{
|
|
SwapHash: f.InstantOut.SwapHash[:],
|
|
},
|
|
)
|
|
if err != nil {
|
|
pollPaymentTries++
|
|
if pollPaymentTries > 20 {
|
|
return f.handleErrorAndUnlockReservations(ctx, err)
|
|
}
|
|
}
|
|
if res != nil && res.Accepted {
|
|
return OnPaymentAccepted
|
|
}
|
|
timer.Reset(defaultPollPaymentTime)
|
|
}
|
|
}
|
|
}
|
|
|
|
// BuildHTLCAction creates the htlc transaction, exchanges nonces with
|
|
// the server and sends the htlc signatures to the server.
|
|
func (f *FSM) BuildHTLCAction(ctx context.Context,
|
|
eventCtx fsm.EventContext) fsm.EventType {
|
|
|
|
htlcSessions, htlcClientNonces, err := f.InstantOut.createMusig2Session(
|
|
ctx, f.cfg.Signer,
|
|
)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(ctx, err)
|
|
}
|
|
|
|
f.htlcMusig2Sessions = htlcSessions
|
|
defer func() {
|
|
err := cleanupMuSig2Sessions(
|
|
ctx, f.cfg.Signer, f.htlcMusig2Sessions,
|
|
)
|
|
if err != nil {
|
|
f.Errorf("unable to clean up HTLC MuSig2 sessions: %v", err)
|
|
}
|
|
f.htlcMusig2Sessions = nil
|
|
}()
|
|
|
|
// Send the server the client nonces.
|
|
htlcInitRes, err := f.cfg.InstantOutClient.InitHtlcSig(
|
|
ctx,
|
|
&swapserverrpc.InitHtlcSigRequest{
|
|
SwapHash: f.InstantOut.SwapHash[:],
|
|
HtlcClientNonces: htlcClientNonces,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(ctx, err)
|
|
}
|
|
|
|
if len(htlcInitRes.HtlcServerNonces) != len(f.InstantOut.Reservations) {
|
|
return f.handleErrorAndUnlockReservations(
|
|
ctx, errors.New("invalid number of server nonces"),
|
|
)
|
|
}
|
|
|
|
htlcServerNonces, err := toNonces(htlcInitRes.HtlcServerNonces)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(ctx, err)
|
|
}
|
|
|
|
minRelayFee, err := f.cfg.Wallet.MinRelayFee(ctx)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(ctx, err)
|
|
}
|
|
|
|
// Now that our nonces are set, we can create and sign the htlc
|
|
// transaction.
|
|
htlcTx, err := f.InstantOut.createHtlcTransaction(
|
|
f.cfg.Network, minRelayFee,
|
|
)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(ctx, err)
|
|
}
|
|
|
|
// Next we'll get our sweep tx signatures.
|
|
htlcSigs, err := f.InstantOut.signMusig2Tx(
|
|
ctx, f.cfg.Signer, htlcTx, f.htlcMusig2Sessions,
|
|
htlcServerNonces,
|
|
)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(ctx, err)
|
|
}
|
|
|
|
// Send the server the htlc signatures.
|
|
htlcRes, err := f.cfg.InstantOutClient.PushHtlcSig(
|
|
ctx,
|
|
&swapserverrpc.PushHtlcSigRequest{
|
|
SwapHash: f.InstantOut.SwapHash[:],
|
|
ClientSigs: htlcSigs,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(ctx, err)
|
|
}
|
|
|
|
// We can now finalize the htlc transaction.
|
|
htlcTx, err = f.InstantOut.finalizeMusig2Transaction(
|
|
ctx, f.cfg.Signer, f.htlcMusig2Sessions, htlcTx,
|
|
htlcRes.ServerSigs,
|
|
)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(ctx, err)
|
|
}
|
|
|
|
f.InstantOut.finalizedHtlcTx = htlcTx
|
|
|
|
return OnHtlcSigReceived
|
|
}
|
|
|
|
// PushPreimageAction pushes the preimage to the server. It also creates the
|
|
// sweepless sweep transaction and sends the signatures to the server. Finally,
|
|
// it publishes the sweepless sweep transaction. If any of the steps after
|
|
// pushing the preimage fail, the htlc timeout transaction will be published.
|
|
func (f *FSM) PushPreimageAction(ctx context.Context,
|
|
eventCtx fsm.EventContext) fsm.EventType {
|
|
|
|
// A recovered swap may have been offline long enough that the server's
|
|
// reservation timeout is now close. Fall back to the already finalized
|
|
// HTLC instead of revealing the preimage without enough time to publish
|
|
// that safety transaction.
|
|
if _, ok := eventCtx.(*RecoverInstantOutCtx); ok {
|
|
info, err := f.cfg.LndClient.GetInfo(ctx)
|
|
if err != nil {
|
|
f.LastActionError = fmt.Errorf(
|
|
"unable to get recovery chain height: %w", err,
|
|
)
|
|
|
|
return OnErrorPublishHtlc
|
|
}
|
|
|
|
currentHeight := int64(info.BlockHeight)
|
|
minHtlcExpiry := currentHeight +
|
|
int64(htlcRecoverySafetyDelta)
|
|
if int64(f.InstantOut.CltvExpiry) < minHtlcExpiry {
|
|
f.LastActionError = fmt.Errorf("instant out HTLC expires at "+
|
|
"height %d, before recovery safety height %d",
|
|
f.InstantOut.CltvExpiry, minHtlcExpiry)
|
|
|
|
return OnErrorPublishHtlc
|
|
}
|
|
|
|
minReservationExpiry := currentHeight +
|
|
int64(htlcExpiryDelta)
|
|
for _, res := range f.InstantOut.Reservations {
|
|
if int64(res.Expiry) >= minReservationExpiry {
|
|
continue
|
|
}
|
|
|
|
f.LastActionError = fmt.Errorf("reservation %x expires at "+
|
|
"height %d, before recovery safety height %d",
|
|
res.ID, res.Expiry, minReservationExpiry)
|
|
|
|
return OnErrorPublishHtlc
|
|
}
|
|
}
|
|
|
|
// First we'll create the musig2 context.
|
|
coopSessions, coopClientNonces, err := f.InstantOut.createMusig2Session(
|
|
ctx, f.cfg.Signer,
|
|
)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(ctx, err)
|
|
}
|
|
|
|
f.sweeplessSweepSessions = coopSessions
|
|
defer func() {
|
|
err := cleanupMuSig2Sessions(
|
|
ctx, f.cfg.Signer, f.sweeplessSweepSessions,
|
|
)
|
|
if err != nil {
|
|
f.Errorf("unable to clean up sweep MuSig2 sessions: %v", err)
|
|
}
|
|
f.sweeplessSweepSessions = nil
|
|
}()
|
|
|
|
// Get the feerate for the coop sweep.
|
|
feeRate, err := f.cfg.Wallet.EstimateFeeRate(ctx, normalConfTarget)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(ctx, err)
|
|
}
|
|
|
|
minRelayFee, err := f.cfg.Wallet.MinRelayFee(ctx)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(ctx, err)
|
|
}
|
|
|
|
pushPreImageRes, err := f.cfg.InstantOutClient.PushPreimage(
|
|
ctx,
|
|
&swapserverrpc.PushPreimageRequest{
|
|
Preimage: f.InstantOut.swapPreimage[:],
|
|
ClientNonces: coopClientNonces,
|
|
ClientSweepAddr: f.InstantOut.sweepAddress.String(),
|
|
MusigTxFeeRate: uint64(feeRate),
|
|
},
|
|
)
|
|
// Now that we have revealed the preimage, if any following step fail,
|
|
// we'll need to publish the htlc tx.
|
|
if err != nil {
|
|
f.LastActionError = err
|
|
return OnErrorPublishHtlc
|
|
}
|
|
|
|
// Now that we have the sweepless sweep signatures we can build and
|
|
// publish the sweepless sweep transaction.
|
|
sweepTx, err := f.InstantOut.createSweeplessSweepTx(
|
|
feeRate, minRelayFee,
|
|
)
|
|
if err != nil {
|
|
f.LastActionError = err
|
|
return OnErrorPublishHtlc
|
|
}
|
|
|
|
coopServerNonces, err := toNonces(pushPreImageRes.ServerNonces)
|
|
if err != nil {
|
|
f.LastActionError = err
|
|
return OnErrorPublishHtlc
|
|
}
|
|
|
|
// Next we'll get our sweep tx signatures.
|
|
_, err = f.InstantOut.signMusig2Tx(
|
|
ctx, f.cfg.Signer, sweepTx, f.sweeplessSweepSessions,
|
|
coopServerNonces,
|
|
)
|
|
if err != nil {
|
|
f.LastActionError = err
|
|
return OnErrorPublishHtlc
|
|
}
|
|
|
|
// Now we'll finalize the sweepless sweep transaction.
|
|
sweepTx, err = f.InstantOut.finalizeMusig2Transaction(
|
|
ctx, f.cfg.Signer, f.sweeplessSweepSessions, sweepTx,
|
|
pushPreImageRes.Musig2SweepSigs,
|
|
)
|
|
if err != nil {
|
|
f.LastActionError = err
|
|
return OnErrorPublishHtlc
|
|
}
|
|
|
|
txLabel := fmt.Sprintf("sweepless-sweep-%v",
|
|
f.InstantOut.swapPreimage.Hash())
|
|
|
|
// Publish the sweepless sweep transaction.
|
|
err = f.cfg.Wallet.PublishTransaction(ctx, sweepTx, txLabel)
|
|
if err != nil {
|
|
f.LastActionError = err
|
|
return OnErrorPublishHtlc
|
|
}
|
|
|
|
f.InstantOut.FinalizedSweeplessSweepTx = sweepTx
|
|
txHash := f.InstantOut.FinalizedSweeplessSweepTx.TxHash()
|
|
|
|
f.InstantOut.SweepTxHash = &txHash
|
|
|
|
return OnSweeplessSweepPublished
|
|
}
|
|
|
|
// WaitForSweeplessSweepConfirmedAction waits for the sweepless sweep
|
|
// transaction to be confirmed.
|
|
func (f *FSM) WaitForSweeplessSweepConfirmedAction(ctx context.Context,
|
|
eventCtx fsm.EventContext) fsm.EventType {
|
|
|
|
pkscript, err := txscript.PayToAddrScript(f.InstantOut.sweepAddress)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
confChan, confErrChan, err := f.cfg.ChainNotifier.
|
|
RegisterConfirmationsNtfn(
|
|
ctx, f.InstantOut.SweepTxHash, pkscript,
|
|
1, f.InstantOut.initiationHeight,
|
|
)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case spendErr := <-confErrChan:
|
|
f.LastActionError = spendErr
|
|
f.Errorf("error listening for sweepless sweep "+
|
|
"confirmation: %v", spendErr)
|
|
|
|
return OnErrorPublishHtlc
|
|
|
|
case conf := <-confChan:
|
|
f.InstantOut.
|
|
sweepConfirmationHeight = conf.BlockHeight
|
|
|
|
return OnSweeplessSweepConfirmed
|
|
}
|
|
}
|
|
}
|
|
|
|
// PublishHtlcAction publishes the htlc transaction and the htlc sweep
|
|
// transaction.
|
|
func (f *FSM) PublishHtlcAction(ctx context.Context,
|
|
eventCtx fsm.EventContext) fsm.EventType {
|
|
|
|
// Publish the htlc transaction.
|
|
label := fmt.Sprintf("htlc-%v", f.InstantOut.swapPreimage.Hash())
|
|
err := f.cfg.Wallet.PublishTransaction(
|
|
ctx, f.InstantOut.finalizedHtlcTx, label,
|
|
)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
txHash := f.InstantOut.finalizedHtlcTx.TxHash()
|
|
f.Debugf("published htlc tx: %v", txHash)
|
|
|
|
// We'll now wait for the htlc to be confirmed.
|
|
confChan, confErrChan, err := f.cfg.ChainNotifier.
|
|
RegisterConfirmationsNtfn(
|
|
ctx, &txHash,
|
|
f.InstantOut.finalizedHtlcTx.TxOut[0].PkScript,
|
|
1, f.InstantOut.initiationHeight,
|
|
)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
for {
|
|
select {
|
|
case spendErr := <-confErrChan:
|
|
return f.HandleError(spendErr)
|
|
|
|
case <-confChan:
|
|
return OnHtlcPublished
|
|
}
|
|
}
|
|
}
|
|
|
|
// PublishHtlcSweepAction publishes the htlc sweep transaction.
|
|
func (f *FSM) PublishHtlcSweepAction(ctx context.Context,
|
|
eventCtx fsm.EventContext) fsm.EventType {
|
|
|
|
// Create a feerate that will confirm the htlc quickly.
|
|
feeRate, err := f.cfg.Wallet.EstimateFeeRate(ctx, urgentConfTarget)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
getInfo, err := f.cfg.LndClient.GetInfo(ctx)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
// We can immediately publish the htlc sweep transaction.
|
|
htlcSweepTx, err := f.InstantOut.generateHtlcSweepTx(
|
|
ctx, f.cfg.Signer, feeRate, f.cfg.Network, getInfo.BlockHeight,
|
|
)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
label := fmt.Sprintf("htlc-sweep-%v", f.InstantOut.swapPreimage.Hash())
|
|
|
|
err = f.cfg.Wallet.PublishTransaction(ctx, htlcSweepTx, label)
|
|
if err != nil {
|
|
log.Errorf("error publishing htlc sweep tx: %v", err)
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
sweepTxHash := htlcSweepTx.TxHash()
|
|
|
|
f.InstantOut.SweepTxHash = &sweepTxHash
|
|
|
|
return OnHtlcSweepPublished
|
|
}
|
|
|
|
// WaitForHtlcSweepConfirmedAction waits for the htlc sweep transaction to be
|
|
// confirmed.
|
|
func (f *FSM) WaitForHtlcSweepConfirmedAction(ctx context.Context,
|
|
eventCtx fsm.EventContext) fsm.EventType {
|
|
|
|
sweepPkScript, err := txscript.PayToAddrScript(
|
|
f.InstantOut.sweepAddress,
|
|
)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
confChan, confErrChan, err := f.cfg.ChainNotifier.RegisterConfirmationsNtfn(
|
|
ctx, f.InstantOut.SweepTxHash, sweepPkScript,
|
|
1, f.InstantOut.initiationHeight,
|
|
)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
f.Debugf("waiting for htlc sweep tx %v to be confirmed",
|
|
f.InstantOut.SweepTxHash)
|
|
|
|
for {
|
|
select {
|
|
case spendErr := <-confErrChan:
|
|
return f.HandleError(spendErr)
|
|
|
|
case conf := <-confChan:
|
|
f.InstantOut.
|
|
sweepConfirmationHeight = conf.BlockHeight
|
|
|
|
return OnHtlcSwept
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleErrorAndUnlockReservations handles an error and unlocks the
|
|
// reservations.
|
|
func (f *FSM) handleErrorAndUnlockReservations(ctx context.Context,
|
|
actionErr error) fsm.EventType {
|
|
// We might get here from a canceled context, we create a new context
|
|
// with a timeout to unlock the reservations.
|
|
cleanupCtx, cancel := context.WithTimeout(
|
|
context.WithoutCancel(ctx), time.Second*30,
|
|
)
|
|
defer cancel()
|
|
|
|
// Unlock the reservations.
|
|
var unlockErr error
|
|
for _, reservation := range f.InstantOut.Reservations {
|
|
err := f.cfg.ReservationManager.UnlockReservation(
|
|
cleanupCtx, reservation.ID,
|
|
)
|
|
if err != nil {
|
|
f.Errorf("error unlocking reservation: %v", err)
|
|
unlockErr = errors.Join(unlockErr, err)
|
|
}
|
|
}
|
|
|
|
// We're also sending the server a cancel message so that it can
|
|
// release the reservations. This can be done in a goroutine as we
|
|
// wan't to fail the fsm early.
|
|
go func() {
|
|
cancelCtx, cancel := context.WithTimeout(
|
|
context.WithoutCancel(ctx), time.Second*30,
|
|
)
|
|
defer cancel()
|
|
_, cancelErr := f.cfg.InstantOutClient.CancelInstantSwap(
|
|
cancelCtx, &swapserverrpc.CancelInstantSwapRequest{
|
|
SwapHash: f.InstantOut.SwapHash[:],
|
|
},
|
|
)
|
|
if cancelErr != nil {
|
|
// We'll log the error but not return it as we want to return the
|
|
// original error.
|
|
f.Debugf("error sending cancel message: %v", cancelErr)
|
|
}
|
|
}()
|
|
|
|
// Preserve the action failure when cleanup also fails. If cleanup was
|
|
// the only failure, report it to the state machine.
|
|
if actionErr != nil {
|
|
return f.HandleError(actionErr)
|
|
}
|
|
|
|
return f.HandleError(unlockErr)
|
|
}
|
|
|
|
func getMaxRoutingFee(amt btcutil.Amount) btcutil.Amount {
|
|
return swap.CalcFee(amt, maxRoutingFeeBase, maxRoutingFeeRate)
|
|
}
|