package loopin

import (
	"context"
	"crypto/rand"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/btcsuite/btcd/btcec/v2"
	"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
	"github.com/btcsuite/btcd/btcutil"
	"github.com/btcsuite/btcd/txscript"
	"github.com/btcsuite/btcd/wire"
	"github.com/btcsuite/btcwallet/chain"
	"github.com/lightninglabs/lndclient"
	"github.com/lightninglabs/loop"
	"github.com/lightninglabs/loop/fsm"
	"github.com/lightninglabs/loop/staticaddr/deposit"
	"github.com/lightninglabs/loop/staticaddr/staticutil"
	"github.com/lightninglabs/loop/staticaddr/version"
	"github.com/lightninglabs/loop/swap"
	"github.com/lightninglabs/loop/swapserverrpc"
	"github.com/lightningnetwork/lnd/chainntnfs"
	"github.com/lightningnetwork/lnd/input"
	"github.com/lightningnetwork/lnd/invoices"
	"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
	"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
	"github.com/lightningnetwork/lnd/lntypes"
	"github.com/lightningnetwork/lnd/lnwallet"
	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
	"github.com/lightningnetwork/lnd/lnwire"
)

const (
	defaultConfTarget = 3

	DefaultPaymentTimeoutSeconds = 60

	defaultInvoiceCleanupTimeout = 5 * time.Second

	monitorRetryDelay = time.Second
)

var (
	// ErrFeeTooHigh is returned if the server sets a fee rate for the htlc
	// tx that is too high. We prevent here against a low htlc timeout sweep
	// amount.
	ErrFeeTooHigh = errors.New("server htlc tx fee is higher than the " +
		"configured allowed maximum")

	// ErrBackupFeeTooHigh is returned if the server sets a fee rate for the
	// htlc backup tx that is too high. We prevent here against a low htlc
	// timeout sweep amount.
	ErrBackupFeeTooHigh = errors.New("server htlc backup tx fee is " +
		"higher than the configured allowed maximum")
)

// InitHtlcAction is executed if all loop-in information has been validated. We
// assemble a loop-in request and send it to the server.
func (f *FSM) InitHtlcAction(ctx context.Context,
	_ fsm.EventContext) fsm.EventType {

	var event fsm.EventType
	invoiceNeedsCleanup := false
	defer func() {
		// If we created the private invoice but failed before persisting the
		// swap, cancel it so retries do not accumulate orphan invoices.
		if !invoiceNeedsCleanup || event != fsm.OnError {
			return
		}

		if err := f.cancelSwapInvoice(); err != nil {
			f.Warnf("unable to clean up invoice for swap %v: %v",
				f.loopIn.SwapHash, err)
		}
	}()

	returnError := func(err error) fsm.EventType {
		event = f.HandleError(err)

		return event
	}

	// Lock the deposits and transition them to the LoopingIn state.
	err := f.cfg.DepositManager.TransitionDeposits(
		ctx, f.loopIn.Deposits, deposit.OnLoopInInitiated,
		deposit.LoopingIn,
	)
	if err != nil {
		err = fmt.Errorf("unable to loop-in deposits: %w", err)

		return returnError(err)
	}

	// Calculate the swap invoice amount. The server needs to pay us the
	// swap amount minus the fees that the server charges for the swap. The
	// swap amount is either the total value of the selected deposits, or
	// the selected amount if a specific amount was requested.
	totalDepositAmount := f.loopIn.TotalDepositAmount()
	swapAmount := totalDepositAmount
	var changeAmount btcutil.Amount
	var hasChange bool
	if f.loopIn.SelectedAmount > 0 {
		swapAmount = f.loopIn.SelectedAmount
		changeAmount = totalDepositAmount - swapAmount
		hasChange = changeAmount > 0 && changeAmount < totalDepositAmount
	}
	swapInvoiceAmt := swapAmount - f.loopIn.QuotedSwapFee

	// Generate random preimage.
	var swapPreimage lntypes.Preimage
	if _, err = rand.Read(swapPreimage[:]); err != nil {
		err = fmt.Errorf("unable to create random swap preimage: %w",
			err)

		return returnError(err)
	}
	f.loopIn.SwapPreimage = swapPreimage
	f.loopIn.SwapHash = swapPreimage.Hash()

	// Derive a client key for the HTLC.
	keyDesc, err := f.cfg.WalletKit.DeriveNextKey(
		ctx, swap.StaticAddressKeyFamily,
	)
	if err != nil {
		err = fmt.Errorf("unable to derive client htlc key: %w", err)

		return returnError(err)
	}
	f.loopIn.ClientPubkey = keyDesc.PubKey
	f.loopIn.HtlcKeyLocator = keyDesc.KeyLocator

	// Create the swap invoice in lnd.
	_, swapInvoice, err := f.cfg.LndClient.AddInvoice(
		ctx, &invoicesrpc.AddInvoiceData{
			Preimage:   &swapPreimage,
			Value:      lnwire.NewMSatFromSatoshis(swapInvoiceAmt),
			Memo:       "static address loop-in",
			Expiry:     3600 * 24 * 365,
			RouteHints: f.loopIn.RouteHints,
			Private:    true,
		},
	)
	if err != nil {
		err = fmt.Errorf("unable to create swap invoice: %w", err)

		return returnError(err)
	}
	f.loopIn.SwapInvoice = swapInvoice

	// From here until CreateLoopIn succeeds, any error path would otherwise
	// leave behind a live invoice with no persisted swap to recover it.
	invoiceNeedsCleanup = true

	f.loopIn.ProtocolVersion = version.AddressProtocolVersion(
		version.CurrentRPCProtocolVersion(),
	)

	loopInReq := &swapserverrpc.ServerStaticAddressLoopInRequest{
		SwapHash:              f.loopIn.SwapHash[:],
		DepositOutpoints:      f.loopIn.DepositOutpoints,
		Amount:                uint64(f.loopIn.SelectedAmount),
		HtlcClientPubKey:      f.loopIn.ClientPubkey.SerializeCompressed(),
		SwapInvoice:           f.loopIn.SwapInvoice,
		ProtocolVersion:       version.CurrentRPCProtocolVersion(),
		UserAgent:             loop.UserAgent(f.loopIn.Initiator),
		PaymentTimeoutSeconds: f.loopIn.PaymentTimeoutSeconds,
		Fast:                  f.loopIn.Fast,
	}
	if f.loopIn.LastHop != nil {
		loopInReq.LastHop = f.loopIn.LastHop
	}

	loopInResp, err := f.cfg.Server.ServerStaticAddressLoopIn(
		ctx, loopInReq,
	)
	if err != nil {
		err = fmt.Errorf("unable to initiate the loop-in with the "+
			"server: %w", err)

		return returnError(err)
	}

	// Pushing empty sigs signals the server that we abandoned the swap
	// attempt.
	pushEmptySigs := func() {
		_, err = f.cfg.Server.PushStaticAddressHtlcSigs(
			ctx, &swapserverrpc.PushStaticAddressHtlcSigsRequest{
				SwapHash: f.loopIn.SwapHash[:],
			},
		)
		if err != nil {
			log.Warnf("unable to push htlc tx sigs to server: %v",
				err)
		}
	}

	serverPubkey, err := btcec.ParsePubKey(loopInResp.HtlcServerPubKey)
	if err != nil {
		pushEmptySigs()
		err = fmt.Errorf("unable to parse server pubkey: %w", err)

		return returnError(err)
	}
	f.loopIn.ServerPubkey = serverPubkey

	// Validate if the response parameters are outside our allowed range
	// preventing us from continuing with a swap.
	err = f.cfg.ValidateLoopInContract(
		int32(f.loopIn.InitiationHeight), loopInResp.HtlcExpiry,
	)
	if err != nil {
		pushEmptySigs()
		err = fmt.Errorf("server response parameters are outside "+
			"our allowed range: %w", err)

		return returnError(err)
	}

	f.loopIn.HtlcCltvExpiry = loopInResp.HtlcExpiry
	f.htlcServerNonces, err = toNonces(loopInResp.StandardHtlcInfo.Nonces)
	if err != nil {
		pushEmptySigs()
		err = fmt.Errorf("unable to convert server nonces: %w", err)

		return returnError(err)
	}
	f.htlcServerNoncesHighFee, err = toNonces(
		loopInResp.HighFeeHtlcInfo.Nonces,
	)
	if err != nil {
		pushEmptySigs()

		return returnError(err)
	}
	f.htlcServerNoncesExtremelyHighFee, err = toNonces(
		loopInResp.ExtremeFeeHtlcInfo.Nonces,
	)
	if err != nil {
		pushEmptySigs()

		return returnError(err)
	}

	// We need to defend against the server setting high fees for the htlc
	// tx since we might have to sweep the timeout path. We maximally allow
	// a configured percentage of the swap value to be spent on fees.
	amt := float64(swapAmount)
	maxHtlcTxFee := btcutil.Amount(amt *
		f.cfg.MaxStaticAddrHtlcFeePercentage)

	maxHtlcTxBackupFee := btcutil.Amount(amt *
		f.cfg.MaxStaticAddrHtlcBackupFeePercentage)

	htlcWeight := f.loopIn.htlcWeight(hasChange)
	feeRate := chainfee.SatPerKWeight(loopInResp.StandardHtlcInfo.FeeRate)
	fee := feeRate.FeeForWeight(htlcWeight)
	highFeeRate := chainfee.SatPerKWeight(loopInResp.HighFeeHtlcInfo.FeeRate)
	highFee := highFeeRate.FeeForWeight(htlcWeight)
	extremelyHighFeeRate := chainfee.SatPerKWeight(
		loopInResp.ExtremeFeeHtlcInfo.FeeRate,
	)
	extremelyHighFee := extremelyHighFeeRate.FeeForWeight(htlcWeight)

	f.Debugf("htlc fee validation: "+
		"deposit_count=%d, total_deposit=%v, "+
		"swap_amount=%v, change_amount=%v, has_change=%v, "+
		"htlc_weight=%v, standard_fee_rate=%v, standard_fee=%v, "+
		"high_fee_rate=%v, high_fee=%v, extreme_fee_rate=%v, "+
		"extreme_fee=%v, max_fee=%v, max_backup_fee=%v",
		len(f.loopIn.Deposits), totalDepositAmount, swapAmount,
		changeAmount, hasChange, htlcWeight, feeRate, fee, highFeeRate,
		highFee, extremelyHighFeeRate, extremelyHighFee, maxHtlcTxFee,
		maxHtlcTxBackupFee)

	if fee > maxHtlcTxFee {
		// Abort the swap by pushing empty sigs to the server.
		pushEmptySigs()

		f.Errorf("server standard htlc tx fee is higher than the "+
			"configured allowed maximum: %v > %v "+
			"(fee_rate=%v, weight=%v)",
			fee, maxHtlcTxFee, feeRate, htlcWeight)

		return returnError(ErrFeeTooHigh)
	}
	f.loopIn.HtlcTxFeeRate = feeRate

	if highFee > maxHtlcTxBackupFee {
		// Abort the swap by pushing empty sigs to the server.
		pushEmptySigs()

		f.Errorf("server high-fee htlc backup tx fee is higher "+
			"than the configured allowed maximum: %v > %v "+
			"(fee_rate=%v, weight=%v)",
			highFee, maxHtlcTxBackupFee, highFeeRate, htlcWeight)

		return returnError(ErrFeeTooHigh)
	}
	f.loopIn.HtlcTxHighFeeRate = highFeeRate

	if extremelyHighFee > maxHtlcTxBackupFee {
		// Abort the swap by pushing empty sigs to the server.
		pushEmptySigs()

		f.Errorf("server extreme-fee htlc backup tx fee is "+
			"higher than the configured allowed maximum: %v > %v "+
			"(fee_rate=%v, weight=%v)",
			extremelyHighFee, maxHtlcTxBackupFee,
			extremelyHighFeeRate, htlcWeight)

		return returnError(ErrFeeTooHigh)
	}
	f.loopIn.HtlcTxExtremelyHighFeeRate = extremelyHighFeeRate

	// Derive the sweep address for the htlc timeout sweep tx.
	sweepAddress, err := f.cfg.WalletKit.NextAddr(
		ctx, lnwallet.DefaultAccountName,
		walletrpc.AddressType_TAPROOT_PUBKEY, false,
	)
	if err != nil {
		pushEmptySigs()
		err = fmt.Errorf("unable to derive htlc timeout sweep "+
			"address: %w", err)

		return returnError(err)
	}
	f.loopIn.HtlcTimeoutSweepAddress = sweepAddress

	// Once the htlc tx is initiated, we store the loop-in in the database.
	err = f.cfg.Store.CreateLoopIn(ctx, f.loopIn)
	if err != nil {
		pushEmptySigs()
		err = fmt.Errorf("unable to store loop-in in db: %w", err)

		return returnError(err)
	}

	// Once the swap is stored, restart/recovery code owns invoice lifecycle.
	invoiceNeedsCleanup = false

	err = f.sendUpdate(ctx)
	if err != nil {
		f.Errorf("Error sending loop-in update: %v", err)
	}

	event = OnHtlcInitiated

	return event
}

// cancelSwapInvoice cancels the current swap invoice using a detached,
// timeout-limited context. Callers that must not proceed while the invoice may
// still be payable can use the returned error to retry.
func (f *FSM) cancelSwapInvoice() error {
	if f.loopIn.SwapHash == (lntypes.Hash{}) {
		return nil
	}

	cleanupCtx, cancel := context.WithTimeout(
		context.Background(), defaultInvoiceCleanupTimeout,
	)
	defer cancel()

	err := f.cfg.InvoicesClient.CancelInvoice(cleanupCtx, f.loopIn.SwapHash)
	return err
}

// handleInvoiceUpdate applies the monitor state's invoice-update semantics and
// reports whether the update produced a terminal event.
func (f *FSM) handleInvoiceUpdate(update lndclient.InvoiceUpdate) (
	fsm.EventType, bool) {

	switch update.State {
	case invoices.ContractOpen:
		return fsm.NoOp, false

	case invoices.ContractAccepted:
		return fsm.NoOp, false

	case invoices.ContractSettled:
		f.Debugf("received off-chain payment update %v", update.State)
		return OnPaymentReceived, true

	case invoices.ContractCanceled:
		// If the invoice was canceled we only log here since we still need
		// to monitor until the htlc timed out.
		log.Warnf("invoice for swap hash %v canceled", f.loopIn.SwapHash)
		return fsm.NoOp, false

	default:
		// An unknown state is not evidence that the invoice can no longer
		// settle. Keep monitoring rather than leaving the deposits available
		// for reuse.
		f.Warnf("unexpected invoice state %v for swap hash %v",
			update.State, f.loopIn.SwapHash)

		return fsm.NoOp, false
	}
}

// selectedDepositConfirmationHeights returns current confirmation heights for
// the original deposit outpoints selected by this loop-in.
func selectedDepositConfirmationHeights(
	loopIn *StaticAddressLoopIn) map[string]int64 {

	confirmations := make(map[string]int64, len(loopIn.Deposits))
	outpoints := make(map[string]struct{}, len(loopIn.DepositOutpoints))
	for _, outpoint := range loopIn.DepositOutpoints {
		outpoints[outpoint] = struct{}{}
	}

	for _, d := range loopIn.Deposits {
		if d == nil {
			continue
		}

		outpoint := d.OutPoint.String()
		confirmationHeight := d.GetConfirmationHeight()

		if _, ok := outpoints[outpoint]; !ok {
			continue
		}

		confirmations[outpoint] = confirmationHeight
	}

	return confirmations
}

// refreshSelectedDeposits reloads the loop-in's selected deposits from the
// deposit manager/store so recovery does not rely on stale deposit snapshots.
func (f *FSM) refreshSelectedDeposits(ctx context.Context) error {
	if f.cfg.DepositManager == nil || len(f.loopIn.DepositOutpoints) == 0 {
		return nil
	}

	err := f.cfg.DepositManager.EnsureDepositsFresh(ctx)
	if err != nil {
		return fmt.Errorf("unable to refresh deposit wallet view: %w", err)
	}

	const ignoreUnknownOutpoints = false
	deposits, err := f.cfg.DepositManager.DepositsForOutpoints(
		ctx, f.loopIn.DepositOutpoints, ignoreUnknownOutpoints,
	)
	if err != nil {
		return err
	}

	if len(deposits) != len(f.loopIn.DepositOutpoints) {
		return fmt.Errorf("expected %d selected deposits, got %d",
			len(f.loopIn.DepositOutpoints), len(deposits))
	}

	f.loopIn.Deposits = deposits

	return nil
}

// legacyMinConfsReached returns true once every original deposit is confirmed
// and the youngest original deposit has reached the legacy confirmation target.
func legacyMinConfsReached(outpoints []string,
	confirmationHeights map[string]int64, currentHeight int32) bool {

	if currentHeight <= 0 || len(outpoints) == 0 {
		return false
	}

	youngestConfirmation := int64(0)
	for _, outpoint := range outpoints {
		confirmationHeight, ok := confirmationHeights[outpoint]
		if !ok || confirmationHeight <= 0 {
			return false
		}

		if confirmationHeight > youngestConfirmation {
			youngestConfirmation = confirmationHeight
		}
	}

	return int64(currentHeight) >= youngestConfirmation+deposit.MinConfs-1
}

// shouldStartLegacyConfirmationFallback reports whether the local MinConfs
// payment deadline fallback should be armed at the current block height.
//
// The primary path starts the deadline from a server risk-accepted notification.
// This fallback preserves the legacy client-side MinConfs behavior when no risk
// decision has been observed locally: once every original deposit reaches
// MinConfs, the client treats that as enough confirmation-risk clearance to
// start the payment window. The selected deposits are refreshed first so
// recovered swaps do not depend on stale in-memory deposit snapshots.
func (f *FSM) shouldStartLegacyConfirmationFallback(ctx context.Context,
	currentHeight int32) bool {

	err := f.refreshSelectedDeposits(ctx)
	if err != nil {
		f.Warnf("unable to refresh selected deposits for legacy "+
			"confirmation fallback: %v", err)

		return false
	}

	depositConfirmationHeights := selectedDepositConfirmationHeights(
		f.loopIn,
	)

	return legacyMinConfsReached(
		f.loopIn.DepositOutpoints, depositConfirmationHeights,
		currentHeight,
	)
}

// originalDepositOutpointUnavailable checks the original selected deposit
// outpoints against the chain backend's UTXO view.
func (f *FSM) originalDepositOutpointUnavailable(ctx context.Context) (
	bool, error) {

	if f.cfg.TxOutChecker == nil {
		return false, nil
	}

	if len(f.loopIn.DepositOutpoints) == 0 {
		return false, nil
	}

	outpoints := make([]wire.OutPoint, len(f.loopIn.DepositOutpoints))
	for i, outpointStr := range f.loopIn.DepositOutpoints {
		outpoint, err := wire.NewOutPointFromString(outpointStr)
		if err != nil {
			return false, fmt.Errorf("invalid deposit outpoint %q: %w",
				outpointStr, err)
		}

		outpoints[i] = *outpoint
	}

	txOuts, err := f.cfg.TxOutChecker.GetTxOuts(ctx, outpoints)
	if err != nil {
		return false, fmt.Errorf("unable to get txouts: %w", err)
	}

	for _, outpoint := range outpoints {
		if txOuts[outpoint] == nil {
			return true, nil
		}
	}

	return false, nil
}

// SignHtlcTxAction is called if the htlc was initialized and the server
// provided the necessary information to construct the htlc tx. We sign the htlc
// tx and send the signatures to the server.
func (f *FSM) SignHtlcTxAction(ctx context.Context,
	_ fsm.EventContext) fsm.EventType {

	var err error

	outpointUnavailable, err := f.originalDepositOutpointUnavailable(ctx)
	if err != nil {
		return f.HandleError(err)
	}
	if outpointUnavailable {
		err = errors.New("original deposit outpoint no longer available")
		f.Warnf("%v, canceling swap invoice", err)
		if cancelErr := f.cancelSwapInvoice(); cancelErr != nil {
			f.Warnf("unable to cancel invoice for swap %v: %v",
				f.loopIn.SwapHash, cancelErr)
		}

		return f.HandleError(err)
	}

	f.loopIn.AddressParams, err =
		f.cfg.AddressManager.GetStaticAddressParameters(ctx)

	if err != nil {
		err = fmt.Errorf("unable to get static address parameters: "+
			"%w", err)

		return f.HandleError(err)
	}

	f.loopIn.Address, err = f.cfg.AddressManager.GetStaticAddress(ctx)
	if err != nil {
		err = fmt.Errorf("unable to get static address: %w", err)

		return f.HandleError(err)
	}

	err = f.checkDepositsAvailable(ctx)
	if err != nil {
		return f.HandleError(err)
	}

	// Create a musig2 session for each deposit and different htlc tx fee
	// rates.
	createSession := staticutil.CreateMusig2Sessions
	htlcSessions, clientHtlcNonces, err := createSession(
		ctx, f.cfg.Signer, f.loopIn.Deposits, f.loopIn.AddressParams,
		f.loopIn.Address,
	)
	if err != nil {
		err = fmt.Errorf("unable to create musig2 sessions: %w", err)

		return f.HandleError(err)
	}
	defer f.cleanUpSessions(ctx, htlcSessions)

	htlcSessionsHighFee, highFeeNonces, err := createSession(
		ctx, f.cfg.Signer, f.loopIn.Deposits, f.loopIn.AddressParams,
		f.loopIn.Address,
	)
	if err != nil {
		return f.HandleError(err)
	}
	defer f.cleanUpSessions(ctx, htlcSessionsHighFee)

	htlcSessionsExtremelyHighFee, extremelyHighNonces, err := createSession(
		ctx, f.cfg.Signer, f.loopIn.Deposits, f.loopIn.AddressParams,
		f.loopIn.Address,
	)
	if err != nil {
		err = fmt.Errorf("unable to convert nonces: %w", err)
		return f.HandleError(err)
	}
	defer f.cleanUpSessions(ctx, htlcSessionsExtremelyHighFee)

	// Create the htlc txns for different fee rates.
	htlcTx, err := f.loopIn.createHtlcTx(
		f.cfg.ChainParams, f.loopIn.HtlcTxFeeRate,
		f.cfg.MaxStaticAddrHtlcFeePercentage,
	)
	if err != nil {
		return f.HandleError(err)
	}
	htlcTxHighFee, err := f.loopIn.createHtlcTx(
		f.cfg.ChainParams, f.loopIn.HtlcTxHighFeeRate,
		f.cfg.MaxStaticAddrHtlcBackupFeePercentage,
	)
	if err != nil {
		return f.HandleError(err)
	}
	htlcTxExtremelyHighFee, err := f.loopIn.createHtlcTx(
		f.cfg.ChainParams, f.loopIn.HtlcTxExtremelyHighFeeRate,
		f.cfg.MaxStaticAddrHtlcBackupFeePercentage,
	)
	if err != nil {
		err = fmt.Errorf("unable to create the htlc tx: %w", err)
		return f.HandleError(err)
	}

	// Next we'll get our htlc tx signatures for different fee rates.
	htlcSigs, err := f.loopIn.signMusig2Tx(
		ctx, htlcTx, f.cfg.Signer, htlcSessions, f.htlcServerNonces,
	)
	if err != nil {
		err = fmt.Errorf("unable to sign htlc tx: %w", err)
		return f.HandleError(err)
	}

	htlcSigsHighFee, err := f.loopIn.signMusig2Tx(
		ctx, htlcTxHighFee, f.cfg.Signer, htlcSessionsHighFee,
		f.htlcServerNoncesHighFee,
	)
	if err != nil {
		return f.HandleError(err)
	}
	htlcSigsExtremelyHighFee, err := f.loopIn.signMusig2Tx(
		ctx, htlcTxExtremelyHighFee, f.cfg.Signer,
		htlcSessionsExtremelyHighFee, f.htlcServerNoncesExtremelyHighFee,
	)
	if err != nil {
		return f.HandleError(err)
	}

	// Push htlc tx sigs to server.
	pushHtlcReq := &swapserverrpc.PushStaticAddressHtlcSigsRequest{
		SwapHash: f.loopIn.SwapHash[:],
		StandardHtlcInfo: &swapserverrpc.ClientHtlcSigningInfo{
			Nonces: clientHtlcNonces,
			Sigs:   htlcSigs,
		},
		HighFeeHtlcInfo: &swapserverrpc.ClientHtlcSigningInfo{
			Nonces: highFeeNonces,
			Sigs:   htlcSigsHighFee,
		},
		ExtremeFeeHtlcInfo: &swapserverrpc.ClientHtlcSigningInfo{
			Nonces: extremelyHighNonces,
			Sigs:   htlcSigsExtremelyHighFee,
		},
	}
	_, err = f.cfg.Server.PushStaticAddressHtlcSigs(ctx, pushHtlcReq)
	if err != nil {
		err = fmt.Errorf("unable to push htlc tx sigs to server: %w",
			err)

		return f.HandleError(err)
	}

	// Note:
	// From here on we need to monitor for the htlc tx hitting the chain
	// until the invoice is settled because the server can now publish the
	// htlc tx without paying the invoice. In this case we need to wait till
	// the htlc times out and then sweep it back to us.
	return OnHtlcTxSigned
}

// checkDepositsAvailable verifies that all loop-in deposits are still available
// before the client signs the HTLC transaction.
func (f *FSM) checkDepositsAvailable(ctx context.Context) error {
	outpoints, err := f.validateSigningDepositOutpoints()
	if err != nil {
		return err
	}

	if f.cfg.TxOutChecker == nil {
		return nil
	}

	txOuts, err := f.cfg.TxOutChecker.GetTxOuts(ctx, outpoints)
	if err != nil {
		return fmt.Errorf("unable to check deposits: %w", err)
	}

	for _, outpoint := range outpoints {
		if txOuts[outpoint] == nil {
			return fmt.Errorf("deposit %v is no longer available",
				outpoint)
		}
	}

	return nil
}

// validateSigningDepositOutpoints verifies that the current deposit rows match
// the server-side outpoint snapshot before signing the HTLC transaction.
func (f *FSM) validateSigningDepositOutpoints() ([]wire.OutPoint, error) {
	currentOutpoints := f.loopIn.Outpoints()
	if len(f.loopIn.DepositOutpoints) == 0 {
		return currentOutpoints, nil
	}

	if len(f.loopIn.DepositOutpoints) != len(currentOutpoints) {
		return nil, fmt.Errorf("deposit outpoint snapshot has %d "+
			"outpoints, current deposits have %d",
			len(f.loopIn.DepositOutpoints), len(currentOutpoints))
	}

	snapshotOutpoints := make(
		[]wire.OutPoint, len(f.loopIn.DepositOutpoints),
	)
	for i, snapshot := range f.loopIn.DepositOutpoints {
		outpoint, err := wire.NewOutPointFromString(snapshot)
		if err != nil {
			return nil, fmt.Errorf("unable to parse deposit "+
				"outpoint snapshot %q: %w", snapshot, err)
		}

		snapshotOutpoints[i] = *outpoint
		if *outpoint != currentOutpoints[i] {
			return nil, fmt.Errorf("deposit outpoint snapshot "+
				"mismatch at index %d: snapshot %v, "+
				"current %v", i, outpoint, currentOutpoints[i])
		}
	}

	return snapshotOutpoints, nil
}

// cleanUpSessions releases allocated memory of the musig2 sessions.
func (f *FSM) cleanUpSessions(ctx context.Context,
	sessions []*input.MuSig2SessionInfo) {

	for _, s := range sessions {
		err := f.cfg.Signer.MuSig2Cleanup(
			context.WithoutCancel(ctx), s.SessionID,
		)
		if err != nil {
			f.Warnf("unable to cleanup musig2 session: %v", err)
		}
	}
}

// MonitorInvoiceAndHtlcTxAction is called after the htlc tx has been signed by
// us. The server from here on has the ability to publish the htlc tx. If the
// server publishes the htlc tx without paying the invoice, we have to monitor
// for the timeout path and sweep the funds back to us. If, while waiting for
// the htlc timeout, our invoice gets paid, the swap is considered successful,
// and we can stop monitoring the htlc confirmation and continue to sign the
// sweepless sweep.
func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
	_ fsm.EventContext) fsm.EventType {

	retryMonitor := func(err error) fsm.EventType {
		if ctx.Err() != nil {
			return fsm.NoOp
		}

		f.Errorf("monitoring failed: %v, retrying", err)

		invoice, lookupErr := f.cfg.LndClient.LookupInvoice(
			ctx, f.loopIn.SwapHash,
		)
		if lookupErr == nil && invoice.State == invoices.ContractSettled {
			return OnPaymentReceived
		}

		select {
		case <-time.After(monitorRetryDelay):
			return OnRecover

		case <-ctx.Done():
			return fsm.NoOp
		}
	}

	// Subscribe to the state of the swap invoice. If upon restart recovery,
	// we land here and observe that the invoice is already canceled, it can
	// only be the case where a user-provided payment timeout was hit, the
	// invoice got canceled and the timeout of the htlc was not reached yet.
	// So we want to wait until the htlc timeout path opens up so that we
	// could sweep the funds back to us if the server published it without
	// paying the invoice.
	subscribeCtx, cancelInvoiceSubscription := context.WithCancel(ctx)
	defer cancelInvoiceSubscription()

	invoiceUpdateChan, invoiceErrChan, err :=
		f.cfg.InvoicesClient.SubscribeSingleInvoice(
			subscribeCtx, f.loopIn.SwapHash,
		)
	if err != nil {
		if ctx.Err() != nil {
			return fsm.NoOp
		}

		err = fmt.Errorf("unable to subscribe to swap "+
			"invoice: %w", err)

		return retryMonitor(err)
	}

	htlc, err := f.loopIn.getHtlc(f.cfg.ChainParams)
	if err != nil {
		err = fmt.Errorf("unable to get htlc: %w", err)

		return retryMonitor(err)
	}

	// Subscribe to htlc tx confirmation.
	reorgChan := make(chan struct{}, 1)
	// registerHtlcConf registers for the HTLC transaction confirmation using
	// the current reorg channel.
	registerHtlcConf := func() (chan *chainntnfs.TxConfirmation, chan error,
		error) {

		return f.cfg.ChainNotifier.RegisterConfirmationsNtfn(
			ctx, nil, htlc.PkScript, defaultConfTarget,
			int32(f.loopIn.InitiationHeight),
			lndclient.WithReOrgChan(reorgChan),
		)
	}

	htlcConfChan, htlcErrConfChan, err := registerHtlcConf()
	if err != nil {
		if ctx.Err() != nil {
			return fsm.NoOp
		}

		err = fmt.Errorf("unable to monitor htlc tx confirmation: %w",
			err)

		return retryMonitor(err)
	}

	// Subscribe to new blocks.
	registerBlocks := f.cfg.ChainNotifier.RegisterBlockEpochNtfn
	blockChan, blockChanErr, err := registerBlocks(ctx)
	if err != nil {
		if ctx.Err() != nil {
			return fsm.NoOp
		}

		err = fmt.Errorf("unable to subscribe to new blocks: %w", err)

		return retryMonitor(err)
	}

	// The watcher keeps notification normalization and timestamp restoration
	// outside of the swap-state handling below.
	riskWatcher := newConfirmationRiskWatcher(
		f.cfg, f.loopIn.SwapHash, f.Warnf,
	)
	riskUpdateChan, cancelRiskNotificationSubscriptions :=
		riskWatcher.subscribe(ctx)
	defer cancelRiskNotificationSubscriptions()

	// Look up the current invoice state after registering subscriptions so
	// recovery can resume the payment deadline from the latest known state.
	invoice, err := f.cfg.LndClient.LookupInvoice(ctx, f.loopIn.SwapHash)
	if err != nil {
		if ctx.Err() != nil {
			return fsm.NoOp
		}

		// A failed lookup leaves the invoice state unknown. The active
		// subscription can still provide an authoritative update, so keep
		// monitoring and, most importantly, keep the deposits locked.
		f.Warnf("unable to look up invoice by swap hash: %v", err)
		invoice = &lndclient.Invoice{}
	}

	// A settled invoice always takes precedence over a recovered risk
	// rejection or an elapsed payment deadline.
	if invoice.State == invoices.ContractSettled {
		return OnPaymentReceived
	}

	invoiceCanceledForNonPayment := invoice.State == invoices.ContractCanceled
	if invoiceCanceledForNonPayment {
		// If the invoice was canceled previously we end our
		// subscription to invoice updates.
		cancelInvoiceSubscription()
	}

	// Create the swap payment timeout timer after the server confirms
	// confirmation risk was accepted. If a server does not support risk
	// notifications, fall back after the legacy deposit confirmation depth.
	var (
		deadlineChan    <-chan time.Time
		deadlineTimer   *time.Timer
		deadlineStarted bool
	)
	// Stop the payment deadline timer when leaving the monitor action.
	defer func() {
		if deadlineTimer != nil {
			deadlineTimer.Stop()
		}
	}()

	// depositsInState reports whether all selected deposits are currently
	// in the requested state.
	depositsInState := func(state fsm.StateType) bool {
		if len(f.loopIn.Deposits) == 0 {
			return false
		}

		for _, d := range f.loopIn.Deposits {
			if d == nil {
				return false
			}

			if !d.IsInState(state) {
				return false
			}
		}

		return true
	}

	// startPaymentDeadline arms the server payment timeout from the decision
	// time when one is available.
	startPaymentDeadline := func(reason string, startedAt time.Time) {
		if deadlineStarted || invoice.State == invoices.ContractCanceled {
			return
		}

		timeout := f.loopIn.PaymentTimeoutDuration()
		if !startedAt.IsZero() {
			timeout -= time.Since(startedAt)
			if timeout < 0 {
				timeout = 0
			}
		}

		f.Infof("starting payment deadline after %s", reason)
		deadlineTimer = time.NewTimer(timeout)
		deadlineChan = deadlineTimer.C
		deadlineStarted = true
	}

	depositsLockedForHtlcTimeout := depositsInState(
		deposit.SweepHtlcTimeout,
	)

	// transitionDepositsToHtlcTimeout locks deposits into timeout sweeping once
	// the HTLC is confirmed and the invoice cannot be paid.
	transitionDepositsToHtlcTimeout := func(reason string) error {
		if depositsLockedForHtlcTimeout ||
			depositsInState(deposit.SweepHtlcTimeout) {

			depositsLockedForHtlcTimeout = true
			return nil
		}

		depositsToTransition := make(
			[]*deposit.Deposit, 0, len(f.loopIn.Deposits),
		)
		for _, d := range f.loopIn.Deposits {
			if d != nil && d.IsInState(deposit.SweepHtlcTimeout) {
				continue
			}

			depositsToTransition = append(depositsToTransition, d)
		}

		transitionErr := f.cfg.DepositManager.TransitionDeposits(
			ctx, depositsToTransition,
			deposit.OnSweepingHtlcTimeout,
			deposit.SweepHtlcTimeout,
		)
		if transitionErr != nil {
			// WaitForState can report cancellation after the deposit FSMs
			// already reached the target state. Do not turn that shutdown
			// error into success: the monitor must return NoOp and remain
			// recoverable instead of advancing the loop-in FSM.
			if ctx.Err() != nil {
				return ctx.Err()
			}

			// The transition can return an error after every deposit
			// already reached the target state. Treat that as
			// success, but never advance with a partial transition.
			if depositsInState(deposit.SweepHtlcTimeout) {
				depositsLockedForHtlcTimeout = true

				return nil
			}

			return fmt.Errorf("unable to transition deposits to the htlc "+
				"timeout sweeping state after %s: %w",
				reason, transitionErr)
		}
		if !depositsInState(deposit.SweepHtlcTimeout) {
			return fmt.Errorf("not all deposits reached the htlc timeout "+
				"sweeping state after %s", reason)
		}

		depositsLockedForHtlcTimeout = true

		return nil
	}

	// startLegacyFallback starts the payment deadline once the old local
	// minimum-confirmation rule has been satisfied.
	startLegacyFallback := func(reason string, currentHeight int32) {
		if deadlineStarted || invoice.State == invoices.ContractCanceled ||
			f.loopIn.ConfirmationRiskDecision !=
				ConfirmationRiskDecisionNone {

			return
		}

		if f.shouldStartLegacyConfirmationFallback(ctx, currentHeight) {
			decisionTime, ok := riskWatcher.durableDecisionTime(
				ctx, ConfirmationRiskDecisionAccepted,
			)
			if !ok {
				return
			}

			f.loopIn.ConfirmationRiskDecision =
				ConfirmationRiskDecisionAccepted
			f.loopIn.ConfirmationRiskDecisionTime = decisionTime
			startPaymentDeadline(reason, decisionTime)
		}
	}

	// cancelInvoice only marks the invoice canceled after lnd acknowledges
	// the request or the lookup/subscription already observed that state.
	// Failures recover the monitor state without releasing deposits.
	cancelInvoice := func(reason string) (fsm.EventType, bool) {
		if invoice.State != invoices.ContractCanceled {
			f.Errorf("%s, canceling invoice", reason)
			if err := f.cancelSwapInvoice(); err != nil {
				return retryMonitor(err), false
			}
		}

		cancelInvoiceSubscription()
		invoice.State = invoices.ContractCanceled
		invoiceCanceledForNonPayment = true

		return fsm.NoOp, true
	}

	// handleRiskRejected records a server rejection and only exits through
	// the generic error path once the invoice can no longer settle.
	handleRiskRejected := func(reason string,
		decisionTime time.Time) fsm.EventType {

		f.loopIn.ConfirmationRiskDecision =
			ConfirmationRiskDecisionRejected
		f.loopIn.ConfirmationRiskDecisionTime = decisionTime

		event, canceled := cancelInvoice(
			"server rejected confirmation risk wait after " + reason,
		)
		if !canceled {
			return event
		}

		return f.HandleError(fmt.Errorf(
			"server rejected confirmation risk wait after %s", reason,
		))
	}

	switch f.loopIn.ConfirmationRiskDecision {
	case ConfirmationRiskDecisionAccepted:
		startPaymentDeadline(
			"recovered risk accepted notification",
			f.loopIn.ConfirmationRiskDecisionTime,
		)

	case ConfirmationRiskDecisionRejected:
		decisionTime := riskWatcher.decisionTime(
			ctx, ConfirmationRiskDecisionRejected,
		)

		return handleRiskRejected(
			"recovered risk rejection", decisionTime,
		)
	}

	info, err := f.cfg.LndClient.GetInfo(ctx)
	if err != nil {
		f.Warnf("unable to query current height for legacy confirmation "+
			"fallback: %v", err)
	} else {
		startLegacyFallback(
			"legacy confirmation fallback", int32(info.BlockHeight),
		)
	}

	htlcConfirmed := false
	for {
		select {
		case <-htlcConfChan:
			f.Infof("htlc tx confirmed")

			htlcConfirmed = true
			if invoiceCanceledForNonPayment {
				err = transitionDepositsToHtlcTimeout(
					"htlc confirmation after invoice cancellation",
				)
				if err != nil {
					return retryMonitor(err)
				}
			}

		case err = <-htlcErrConfChan:
			if ctx.Err() != nil {
				return fsm.NoOp
			}

			f.Errorf("htlc tx conf chan error, re-registering: "+
				"%v", err)

			// A previous confirmation may no longer be valid if the
			// subscription failed, so reset and wait for a fresh one.
			htlcConfirmed = false

			// Re-register for htlc confirmation.
			htlcConfChan, htlcErrConfChan, err = registerHtlcConf()
			if err != nil {
				if ctx.Err() != nil {
					return fsm.NoOp
				}

				err = fmt.Errorf("unable to re-register for "+
					"htlc tx confirmation: %w", err)

				return retryMonitor(err)
			}

		case <-reorgChan:
			// A reorg happened. We invalidate a previous htlc
			// confirmation and re-register for the next
			// confirmation.
			htlcConfirmed = false

			htlcConfChan, htlcErrConfChan, err = registerHtlcConf()
			if err != nil {
				if ctx.Err() != nil {
					return fsm.NoOp
				}

				err = fmt.Errorf("unable to monitor htlc tx "+
					"confirmation: %v", err)

				return retryMonitor(err)
			}

		case <-deadlineChan:
			deadlineChan = nil

			// If the server didn't pay the invoice on time, we cancel
			// it and keep monitoring the htlc tx. Confirmed HTLC
			// deposits remain locked for timeout sweeping.
			event, canceled := cancelInvoice(
				"timeout waiting for invoice to be paid",
			)
			if !canceled {
				return event
			}
			if htlcConfirmed {
				err = transitionDepositsToHtlcTimeout(
					"payment deadline",
				)
				if err != nil {
					return retryMonitor(err)
				}

				continue
			}

			err = f.unlockDeposits(ctx)
			if err != nil {
				return retryMonitor(fmt.Errorf("unable to unlock deposits "+
					"after payment deadline: %w", err))
			}

		case riskUpdate, ok := <-riskUpdateChan:
			if !ok {
				riskUpdateChan = nil
				continue
			}

			decisionTime := riskWatcher.decisionTime(
				ctx, riskUpdate.decision,
			)
			f.loopIn.ConfirmationRiskDecision = riskUpdate.decision
			f.loopIn.ConfirmationRiskDecisionTime = decisionTime

			switch riskUpdate.decision {
			case ConfirmationRiskDecisionAccepted:
				startPaymentDeadline(
					riskUpdate.reason,
					f.loopIn.ConfirmationRiskDecisionTime,
				)

			case ConfirmationRiskDecisionRejected:
				return handleRiskRejected(
					riskUpdate.reason, decisionTime,
				)
			}

		case currentHeight := <-blockChan:
			startLegacyFallback(
				"legacy confirmation fallback", currentHeight,
			)

			// If the htlc is confirmed but blockChan fires before
			// htlcConfChan, we would wrongfully assume that the
			// htlc tx was not confirmed which would lead to
			// returning OnSwapTimedOut in the code below. This in
			// turn would prevent us from sweeping the htlc timeout
			// path back to us.
			// Hence, we delay the timeout check here by one block
			// to ensure that htlcConfChan fires first.
			if !f.loopIn.isHtlcTimedOut(currentHeight - 1) {
				// If the htlc hasn't timed out yet, we continue
				// monitoring the htlc confirmation and the
				// invoice settlement.
				continue
			}

			f.Infof("htlc timed out at block height %v",
				currentHeight)
			event, canceled := cancelInvoice("htlc timed out")
			if !canceled {
				return event
			}

			if !htlcConfirmed {
				f.Infof("swap timed out, htlc not confirmed")

				// If the htlc hasn't confirmed but the timeout
				// path opened up, and we didn't receive the
				// swap payment, we consider the swap attempt to
				// be failed. Now that the invoice is canceled and
				// the HTLC can no longer confirm, its deposits can be
				// made available again.
				err = f.unlockDeposits(ctx)
				if err != nil {
					return retryMonitor(fmt.Errorf("unable to unlock "+
						"deposits after htlc timeout: %w", err))
				}

				return OnSwapTimedOut
			}

			// If the htlc has confirmed and the timeout path has
			// opened up we sweep the funds back to us.
			err = transitionDepositsToHtlcTimeout("htlc timeout")
			if err != nil {
				return retryMonitor(err)
			}

			return OnSweepHtlcTimeout

		case err = <-blockChanErr:
			if ctx.Err() != nil {
				return fsm.NoOp
			}

			f.Errorf("block subscription error: %v", err)

			return retryMonitor(err)

		case update, ok := <-invoiceUpdateChan:
			if !ok {
				if !invoiceCanceledForNonPayment {
					return retryMonitor(errors.New(
						"invoice update subscription closed",
					))
				}

				invoiceUpdateChan = nil
				continue
			}

			if event, done := f.handleInvoiceUpdate(update); done {
				return event
			}

			invoice.State = update.State
			if update.State == invoices.ContractCanceled {
				invoiceCanceledForNonPayment = true
			}

		case err, ok := <-invoiceErrChan:
			if !ok {
				if !invoiceCanceledForNonPayment {
					return retryMonitor(errors.New(
						"invoice error subscription closed",
					))
				}

				invoiceErrChan = nil
				continue
			}

			if ctx.Err() != nil {
				return fsm.NoOp
			}

			return retryMonitor(fmt.Errorf(
				"invoice subscription error: %w", err,
			))

		case <-ctx.Done():
			return fsm.NoOp
		}
	}
}

// htlcTimeoutSweepRetryDelay is the delay between retries when publishing the
// htlc timeout sweep transaction fails.
const htlcTimeoutSweepRetryDelay = time.Hour

// SweepHtlcTimeoutAction is called if the server published the htlc tx without
// paying the invoice. We wait for the timeout path to open up and sweep the
// funds back to us.
func (f *FSM) SweepHtlcTimeoutAction(ctx context.Context,
	_ fsm.EventContext) fsm.EventType {

	for {
		err := f.createAndPublishHtlcTimeoutSweepTx(ctx)
		if err == nil {
			return OnHtlcTimeoutSweepPublished
		}

		f.Errorf("unable to create and publish htlc timeout sweep "+
			"tx: %v, retrying in %v", err, htlcTimeoutSweepRetryDelay)

		select {
		// The context is cancelled when the server is shutting
		// down. Keep the current state so recovery resumes
		// broadcasting attempts after restart.
		case <-ctx.Done():
			return fsm.NoOp

		case <-time.After(htlcTimeoutSweepRetryDelay):
		}
	}
}

// MonitorHtlcTimeoutSweepAction is called after the htlc timeout sweep tx has
// been published. We monitor the confirmation of the htlc timeout sweep tx and
// finalize the deposits once swept.
func (f *FSM) MonitorHtlcTimeoutSweepAction(ctx context.Context,
	_ fsm.EventContext) fsm.EventType {

	f.Infof("monitoring htlc timeout sweep tx %v",
		f.loopIn.HtlcTimeoutSweepTxHash)

	timeoutSweepPkScript, err := txscript.PayToAddrScript(
		f.loopIn.HtlcTimeoutSweepAddress,
	)
	if err != nil {
		err = fmt.Errorf("unable to convert timeout sweep address to "+
			"pkscript: %w", err)

		return f.HandleError(err)
	}

	htlcTimeoutTxidChan, errChan, err :=
		f.cfg.ChainNotifier.RegisterConfirmationsNtfn(
			ctx, f.loopIn.HtlcTimeoutSweepTxHash,
			timeoutSweepPkScript, defaultConfTarget,
			int32(f.loopIn.InitiationHeight),
		)

	if err != nil {
		if ctx.Err() != nil {
			return fsm.NoOp
		}

		err = fmt.Errorf("unable to register to the htlc timeout "+
			"sweep tx: %w", err)

		return f.HandleError(err)
	}

	for {
		select {
		case err := <-errChan:
			if ctx.Err() != nil {
				return fsm.NoOp
			}

			return f.HandleError(err)

		case conf := <-htlcTimeoutTxidChan:
			err = f.cfg.DepositManager.TransitionDeposits(
				ctx, f.loopIn.Deposits,
				deposit.OnHtlcTimeoutSwept,
				deposit.HtlcTimeoutSwept,
			)
			if err != nil {
				err = fmt.Errorf("unable to transition the "+
					"deposits to the htlc timeout swept "+
					"state: %w", err)

				return f.HandleError(err)
			}

			f.Infof("htlc timeout sweep tx got %d confirmations "+
				"at block %d", defaultConfTarget,
				conf.BlockHeight-defaultConfTarget+1)

			return OnHtlcTimeoutSwept

		case <-ctx.Done():
			return fsm.NoOp
		}
	}
}

// PaymentReceivedAction is called if the invoice was settled. We finalize the
// deposits by transitioning them to the LoopedIn state.
func (f *FSM) PaymentReceivedAction(ctx context.Context,
	_ fsm.EventContext) fsm.EventType {

	// Unlock the deposits and transition them to the LoopedIn state.
	err := f.cfg.DepositManager.TransitionDeposits(
		ctx, f.loopIn.Deposits, deposit.OnLoopedIn, deposit.LoopedIn,
	)
	if err != nil {
		err = fmt.Errorf("payment received, but unable to transition "+
			"deposits into the final state: %w", err)

		return f.HandleError(err)
	}

	return OnSucceeded
}

// UnlockDepositsAction is called if the loop-in failed and its deposits should
// be available in a future loop-in request.
func (f *FSM) UnlockDepositsAction(ctx context.Context,
	_ fsm.EventContext) fsm.EventType {

	if err := f.cancelSwapInvoice(); err != nil {
		f.Warnf("unable to cancel invoice for swap %v: %v",
			f.loopIn.SwapHash, err)
	}

	err := f.unlockDeposits(ctx)
	if err != nil {
		return f.HandleError(err)
	}

	return fsm.OnError
}

func (f *FSM) unlockDeposits(ctx context.Context) error {
	err := f.cfg.DepositManager.TransitionDeposits(
		ctx, f.loopIn.Deposits, fsm.OnError, deposit.Deposited,
	)
	if err != nil {
		return fmt.Errorf("unable to unlock deposits: %w", err)
	}

	return nil
}

// createAndPublishHtlcTimeoutSweepTx creates and publishes the htlc timeout
// sweep transaction.
func (f *FSM) createAndPublishHtlcTimeoutSweepTx(ctx context.Context) error {
	// Get a fee rate.
	feeRate, err := f.cfg.WalletKit.EstimateFeeRate(ctx, defaultConfTarget)
	if err != nil {
		return err
	}

	getInfo, err := f.cfg.LndClient.GetInfo(ctx)
	if err != nil {
		return err
	}

	// Create htlc timeout transaction.
	timeoutTx, err := f.loopIn.createHtlcSweepTx(
		ctx, f.cfg.Signer, f.loopIn.HtlcTimeoutSweepAddress, feeRate,
		f.cfg.ChainParams, getInfo.BlockHeight,
		f.cfg.MaxStaticAddrHtlcFeePercentage,
	)
	if err != nil {
		return fmt.Errorf("unable to create htlc timeout sweep tx: %w",
			err)
	}

	// Broadcast htlc timeout transaction.
	txLabel := fmt.Sprintf(
		"htlc-timeout-sweep-%v", f.loopIn.SwapHash,
	)

	err = f.cfg.WalletKit.PublishTransaction(ctx, timeoutTx, txLabel)
	if err != nil {
		e := err.Error()
		if !strings.Contains(e, "output already spent") ||
			strings.Contains(e, chain.ErrInsufficientFee.Error()) {

			f.Errorf("%v: %v", txLabel, err)
			f.LastActionError = err
			return err
		}
	} else {
		f.Debugf("published htlc timeout sweep with txid: %v",
			timeoutTx.TxHash())

		hash := timeoutTx.TxHash()
		f.loopIn.HtlcTimeoutSweepTxHash = &hash
	}

	return nil
}

// toNonces converts a byte slice to a 66 byte slice.
func toNonces(nonces [][]byte) ([][musig2.PubNonceSize]byte, error) {
	res := make([][musig2.PubNonceSize]byte, 0, len(nonces))
	for _, n := range nonces {
		nonce, err := byteSliceTo66ByteSlice(n)
		if err != nil {
			return nil, err
		}

		res = append(res, nonce)
	}

	return res, nil
}

// byteSliceTo66ByteSlice converts a byte slice to a 66 byte slice.
func byteSliceTo66ByteSlice(b []byte) ([musig2.PubNonceSize]byte, error) {
	if len(b) != musig2.PubNonceSize {
		return [musig2.PubNonceSize]byte{},
			fmt.Errorf("invalid byte slice length")
	}

	var res [musig2.PubNonceSize]byte
	copy(res[:], b)

	return res, nil
}
