package sweepbatcher

import (
	"bytes"
	"context"
	"encoding/hex"
	"errors"
	"fmt"
	"math"
	"sort"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/btcsuite/btcd/blockchain"
	"github.com/btcsuite/btcd/btcec/v2"
	"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
	"github.com/btcsuite/btcd/btcutil"
	"github.com/btcsuite/btcd/btcutil/psbt"
	"github.com/btcsuite/btcd/chaincfg"
	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/btcsuite/btcd/txscript"
	"github.com/btcsuite/btcd/wire"
	"github.com/btcsuite/btclog/v2"
	"github.com/lightninglabs/lndclient"
	"github.com/lightninglabs/loop/loopdb"
	"github.com/lightninglabs/loop/swap"
	sweeppkg "github.com/lightninglabs/loop/sweep"
	"github.com/lightninglabs/loop/utils"
	"github.com/lightningnetwork/lnd/chainntnfs"
	"github.com/lightningnetwork/lnd/clock"
	"github.com/lightningnetwork/lnd/input"
	"github.com/lightningnetwork/lnd/keychain"
	"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
	"github.com/lightningnetwork/lnd/lntypes"
	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)

const (
	// defaultFeeRateStep is the default value by which the batch tx's
	// fee rate is increased when an rbf is attempted.
	defaultFeeRateStep = chainfee.SatPerKWeight(100)

	// batchConfHeight is the default confirmation height of the batch
	// transaction.
	batchConfHeight = 3

	// MaxSweepsPerBatch is the maximum number of sweeps in a single batch.
	// It is needed to prevent sweep tx from becoming non-standard. Max
	// standard transaction is 400k wu, a non-cooperative input is 393 wu.
	MaxSweepsPerBatch = 1000
)

var (
	ErrBatchShuttingDown = errors.New("batch shutting down")
)

// sweep stores any data related to sweeping a specific outpoint.
type sweep struct {
	// swapHash is the hash of the swap that the sweep belongs to.
	// Multiple sweeps may belong to the same swap.
	swapHash lntypes.Hash

	// outpoint is the outpoint being swept.
	outpoint wire.OutPoint

	// value is the value of the outpoint being swept.
	value btcutil.Amount

	// confTarget is the confirmation target of the sweep.
	confTarget int32

	// timeout is the timeout of the swap that the sweep belongs to.
	timeout int32

	// initiationHeight is the height at which the swap was initiated.
	initiationHeight int32

	// htlc is the HTLC that is being swept.
	htlc swap.Htlc

	// preimage is the preimage of the HTLC that is being swept.
	preimage lntypes.Preimage

	// swapInvoicePaymentAddr is the payment address of the swap invoice.
	swapInvoicePaymentAddr [32]byte

	// htlcKeys is the set of keys used to sign the HTLC.
	htlcKeys loopdb.HtlcKeys

	// htlcSuccessEstimator is a function that estimates the weight of the
	// HTLC success script.
	htlcSuccessEstimator func(*input.TxWeightEstimator) error

	// protocolVersion is the protocol version of the swap that the sweep
	// belongs to.
	protocolVersion loopdb.ProtocolVersion

	// isExternalAddr is true if the sweep spends to a non-wallet address.
	isExternalAddr bool

	// destAddr is the destination address of the sweep.
	destAddr btcutil.Address

	// notifier is a collection of channels used to communicate the status
	// of the sweep back to the swap that requested it.
	notifier *SpendNotifier

	// minFeeRate is minimum fee rate that must be used by a batch of
	// the sweep. If it is specified, confTarget is ignored.
	minFeeRate chainfee.SatPerKWeight

	// nonCoopHint is set, if the sweep can not be spent cooperatively and
	// has to be spent using preimage. This is only used in fee estimations
	// when selecting a batch for the sweep to minimize fees.
	nonCoopHint bool

	// coopFailed is set, if we have tried to spend the sweep cooperatively,
	// but it failed. We try to spend a sweep cooperatively only once. This
	// status is not persisted in the DB.
	coopFailed bool

	// presigned is set, if the sweep should be handled in presigned mode.
	presigned bool

	// change is the optional change output of the sweep.
	change *wire.TxOut
}

// batchState is the state of the batch.
type batchState uint8

const (
	// Open is the state in which the batch is able to accept new sweeps.
	Open batchState = 0

	// Closed is the state in which the batch is no longer able to accept
	// new sweeps. NOTE: this state exists only in-memory. In the database
	// it is stored as Open and converted to Closed after a spend
	// notification arrives (quickly after start of Batch.Run).
	Closed batchState = 1

	// Confirmed is the state in which the batch transaction has reached the
	// configured conf height.
	Confirmed batchState = 2
)

// batchConfig is the configuration for a batch.
type batchConfig struct {
	// maxTimeoutDistance is the maximum timeout distance that 2 distinct
	// sweeps can have in the same batch.
	maxTimeoutDistance int32

	// batchConfTarget is the confirmation target of the batch transaction.
	batchConfTarget int32

	// clock provides methods to work with time and timers.
	clock clock.Clock

	// initialDelayProvider provides the delay of first batch publishing
	// after creation. It only affects newly created batches, not batches
	// loaded from DB, so publishing does happen in case of a daemon restart
	// (especially important in case of a crashloop). If a sweep is about to
	// expire (time until timeout is less that 2x initialDelay), then
	// waiting is skipped.
	initialDelayProvider InitialDelayProvider

	// batchPublishDelay is the delay between receiving a new block or
	// initial delay completion and publishing the batch transaction.
	batchPublishDelay time.Duration

	// customFeeRate provides custom min fee rate per swap. The batch uses
	// max of the fee rates of its swaps. In this mode confTarget is
	// ignored and fee bumping by sweepbatcher is disabled.
	customFeeRate FeeRateProvider

	// txLabeler is a function generating a transaction label. It is called
	// before publishing a batch transaction. Batch ID is passed to it.
	txLabeler func(batchID int32) string

	// customMuSig2Signer is a custom signer. If it is set, it is used to
	// create musig2 signatures instead of musig2SignSweep and signerClient.
	// Note that musig2SignSweep must be nil in this case, however signer
	// client must still be provided, as it is used for non-coop spendings.
	customMuSig2Signer SignMuSig2

	// presignedHelper provides methods used when presigned batches are
	// enabled.
	presignedHelper PresignedHelper

	// skippedTxns is the list of previous transactions to ignore when
	// loading the sweeps from DB. This is needed to fix a historical bug.
	skippedTxns map[chainhash.Hash]struct{}

	// chainParams are the chain parameters of the chain that is used by
	// batches.
	chainParams *chaincfg.Params
}

// rbfCache stores data related to our last fee bump.
type rbfCache struct {
	// LastHeight is the last height at which we increased our feerate.
	LastHeight int32

	// FeeRate is the last used fee rate we used to publish a batch tx.
	FeeRate chainfee.SatPerKWeight

	// SkipNextBump instructs updateRbfRate to skip one fee bumping.
	// It is set upon updating FeeRate externally.
	SkipNextBump bool
}

// zeroSweepID is default value for sweep.primarySweepID and batchKit.primaryID.
var zeroSweepID wire.OutPoint

// batch is a collection of sweeps that are published together.
type batch struct {
	// id is the primary identifier of this batch.
	id int32

	// state is the current state of the batch.
	state batchState

	// primarySweepID is the outpoint of the primary sweep in the batch.
	primarySweepID wire.OutPoint

	// sweeps store the sweeps that this batch currently contains.
	sweeps map[wire.OutPoint]sweep

	// currentHeight is the current block height.
	currentHeight int32

	// spendChan is the channel over which spend notifications are received.
	spendChan chan *chainntnfs.SpendDetail

	// spendErrChan is the channel over which spend notifier errors are
	// received.
	spendErrChan chan error

	// confChan is the channel over which confirmation notifications are
	// received.
	confChan chan *chainntnfs.TxConfirmation

	// reorgChan is the channel over which reorg notifications are received.
	reorgChan chan struct{}

	// testReqs is a channel where test requests are received.
	// This is used only in unit tests! The reason to have this is to
	// avoid data races in require.Eventually calls running in parallel
	// to the event loop. See method testRunInEventLoop().
	testReqs chan *testRequest

	// errChan is the channel over which errors are received.
	errChan chan error

	// batchTxid is the transaction that is currently being monitored for
	// confirmations.
	batchTxid *chainhash.Hash

	// batchPkScript is the pkScript of the batch transaction's output.
	batchPkScript []byte

	// batchAddress is the address of the batch transaction's output.
	batchAddress btcutil.Address

	// rbfCache stores data related to the RBF fee bumping mechanism.
	rbfCache rbfCache

	// callEnter is used to sequentialize calls to the batch handler's
	// main event loop.
	callEnter chan struct{}

	// callLeave is used to resume the execution flow of the batch handler's
	// main event loop.
	callLeave chan struct{}

	// stopping signals that the batch is stopping.
	stopping chan struct{}

	// finished signals that the batch has stopped and all child goroutines
	// have finished.
	finished chan struct{}

	// quit is owned by the parent batcher and signals that the batch must
	// stop.
	quit chan struct{}

	// wallet is the wallet client used to create and publish the batch
	// transaction.
	wallet lndclient.WalletKitClient

	// chainNotifier is the chain notifier client used to monitor the
	// blockchain for spends and confirmations.
	chainNotifier lndclient.ChainNotifierClient

	// signerClient is the signer client used to sign the batch transaction.
	signerClient lndclient.SignerClient

	// muSig2SignSweep includes all the required functionality to collect
	// and verify signatures by the swap server in order to cooperatively
	// sweep funds.
	muSig2SignSweep MuSig2SignSweep

	// verifySchnorrSig is a function that verifies a schnorr signature.
	verifySchnorrSig VerifySchnorrSig

	// publishErrorHandler is a function that handles transaction publishing
	// error. By default, it logs all errors as warnings, but "insufficient
	// fee" as Info.
	publishErrorHandler PublishErrorHandler

	// purger is a function that can take a sweep which is being purged and
	// hand it over to the batcher for further processing.
	purger Purger

	// store includes all the database interactions that are needed by the
	// batch.
	store BatcherStore

	// cfg is the configuration for this batch.
	cfg *batchConfig

	// log_ is the logger for this batch.
	log_ atomic.Pointer[btclog.Logger]

	wg sync.WaitGroup
}

// Purger is a function that takes a sweep request and feeds it back to the
// batcher main entry point. The name is inspired by its purpose, which is to
// purge the batch from sweeps that didn't make it to the confirmed tx.
type Purger func(ctx context.Context, sweepReq *SweepRequest) error

// batchKit is a kit of dependencies that are used to initialize a batch. This
// struct is only used as a wrapper for the arguments that are required to
// create a new batch.
type batchKit struct {
	id                  int32
	batchTxid           *chainhash.Hash
	batchPkScript       []byte
	state               batchState
	primaryID           wire.OutPoint
	sweeps              map[wire.OutPoint]sweep
	rbfCache            rbfCache
	wallet              lndclient.WalletKitClient
	chainNotifier       lndclient.ChainNotifierClient
	signerClient        lndclient.SignerClient
	musig2SignSweep     MuSig2SignSweep
	verifySchnorrSig    VerifySchnorrSig
	publishErrorHandler PublishErrorHandler
	purger              Purger
	store               BatcherStore
	log                 btclog.Logger
	quit                chan struct{}
}

// scheduleNextCall schedules the next call to the batch handler's main event
// loop. It returns a function that must be called when the call is finished.
func (b *batch) scheduleNextCall() (func(), error) {
	select {
	case b.callEnter <- struct{}{}:

	case <-b.quit:
		return func() {}, ErrBatcherShuttingDown

	case <-b.stopping:
		return func() {}, ErrBatchShuttingDown

	case <-b.finished:
		return func() {}, ErrBatchShuttingDown
	}

	return func() {
		b.callLeave <- struct{}{}
	}, nil
}

// NewBatch creates a new batch.
func NewBatch(cfg batchConfig, bk batchKit) *batch {
	return &batch{
		// We set the ID to a negative value to flag that this batch has
		// never been persisted, so it needs to be assigned a new ID.
		id:                  -1,
		state:               Open,
		sweeps:              make(map[wire.OutPoint]sweep),
		confChan:            make(chan *chainntnfs.TxConfirmation, 1),
		testReqs:            make(chan *testRequest),
		errChan:             make(chan error, 1),
		callEnter:           make(chan struct{}),
		callLeave:           make(chan struct{}),
		stopping:            make(chan struct{}),
		finished:            make(chan struct{}),
		quit:                bk.quit,
		batchTxid:           bk.batchTxid,
		wallet:              bk.wallet,
		chainNotifier:       bk.chainNotifier,
		signerClient:        bk.signerClient,
		muSig2SignSweep:     bk.musig2SignSweep,
		verifySchnorrSig:    bk.verifySchnorrSig,
		publishErrorHandler: bk.publishErrorHandler,
		purger:              bk.purger,
		store:               bk.store,
		cfg:                 &cfg,
	}
}

// NewBatchFromDB creates a new batch that already existed in storage.
func NewBatchFromDB(cfg batchConfig, bk batchKit) (*batch, error) {
	// Make sure the batch is not empty.
	if len(bk.sweeps) == 0 {
		// This should never happen, as this precondition is already
		// ensured in spinUpBatchFromDB.
		return nil, fmt.Errorf("empty batch is not allowed")
	}

	// Assign batchConfTarget to primary sweep's confTarget.
	for _, sweep := range bk.sweeps {
		if sweep.outpoint == bk.primaryID {
			cfg.batchConfTarget = sweep.confTarget
			break
		}
	}

	b := &batch{
		id:                  bk.id,
		state:               bk.state,
		primarySweepID:      bk.primaryID,
		sweeps:              bk.sweeps,
		confChan:            make(chan *chainntnfs.TxConfirmation, 1),
		testReqs:            make(chan *testRequest),
		errChan:             make(chan error, 1),
		callEnter:           make(chan struct{}),
		callLeave:           make(chan struct{}),
		stopping:            make(chan struct{}),
		finished:            make(chan struct{}),
		quit:                bk.quit,
		batchTxid:           bk.batchTxid,
		batchPkScript:       bk.batchPkScript,
		rbfCache:            bk.rbfCache,
		wallet:              bk.wallet,
		chainNotifier:       bk.chainNotifier,
		signerClient:        bk.signerClient,
		muSig2SignSweep:     bk.musig2SignSweep,
		verifySchnorrSig:    bk.verifySchnorrSig,
		publishErrorHandler: bk.publishErrorHandler,
		purger:              bk.purger,
		store:               bk.store,
		cfg:                 &cfg,
	}

	b.setLog(bk.log)

	return b, nil
}

// log returns current logger.
func (b *batch) log() btclog.Logger {
	return *b.log_.Load()
}

// setLog atomically replaces the logger.
func (b *batch) setLog(logger btclog.Logger) {
	b.log_.Store(&logger)
}

// Debugf logs a message with level DEBUG.
func (b *batch) Debugf(format string, params ...interface{}) {
	b.log().Debugf(format, params...)
}

// Infof logs a message with level INFO.
func (b *batch) Infof(format string, params ...interface{}) {
	b.log().Infof(format, params...)
}

// Warnf logs a message with level WARN.
func (b *batch) Warnf(format string, params ...interface{}) {
	b.log().Warnf(format, params...)
}

// Errorf logs a message with level ERROR.
func (b *batch) Errorf(format string, params ...interface{}) {
	b.log().Errorf(format, params...)
}

// checkSweepToAdd checks if a sweep can be added or updated in the batch. The
// caller must lock the event loop using scheduleNextCall. The function returns
// if the sweep already exists in the batch. If presigned mode is enabled, the
// result depends on the outcome of the method presignedHelper.SignTx for a
// non-empty batch. For an empty batch, the input needs to pass
// PresignSweepsGroup.
func (b *batch) checkSweepToAdd(_ context.Context, sweep *sweep) (bool, error) {
	// If the provided sweep is nil, we can't proceed with any checks, so
	// we just return early.
	if sweep == nil {
		return false, fmt.Errorf("the sweep is nil")
	}

	// Before we run through the acceptance checks, let's just see if this
	// sweep is already in our batch. In that case, just update the sweep.
	if _, ok := b.sweeps[sweep.outpoint]; ok {
		return true, nil
	}

	// Enforce MaxSweepsPerBatch. If there are already too many sweeps in
	// the batch, do not add another sweep to prevent the tx from becoming
	// non-standard.
	if len(b.sweeps) >= MaxSweepsPerBatch {
		return false, fmt.Errorf("the batch has already too many "+
			"sweeps %d >= %d", len(b.sweeps), MaxSweepsPerBatch)
	}

	// Since all the actions of the batch happen sequentially, we could
	// arrive here after the batch got closed because of a spend. In this
	// case we cannot add the sweep to this batch.
	if b.state != Open {
		return false, fmt.Errorf("the batch state (%v) is not open",
			b.state)
	}

	// If this batch contains a single sweep that spends to a non-wallet
	// address, or the incoming sweep is spending to non-wallet address,
	// we cannot add this sweep to the batch.
	for _, s := range b.sweeps {
		if s.isExternalAddr {
			return false, fmt.Errorf("the batch already has a "+
				"sweep %x with an external address",
				s.swapHash[:6])
		}

		if sweep.isExternalAddr {
			return false, fmt.Errorf("the batch is not empty and "+
				"new sweep %x has an external address",
				sweep.swapHash[:6])
		}
	}

	// Check the timeout of the incoming sweep against the timeout of all
	// already contained sweeps. If that difference exceeds the configured
	// maximum we cannot add this sweep.
	for _, s := range b.sweeps {
		timeoutDistance :=
			int32(math.Abs(float64(sweep.timeout - s.timeout)))

		if timeoutDistance > b.cfg.maxTimeoutDistance {
			return false, fmt.Errorf("too long timeout distance "+
				"between the batch and sweep %x: %d > %d",
				sweep.swapHash[:6], timeoutDistance,
				b.cfg.maxTimeoutDistance)
		}
	}

	// Everything is ok, the sweep can be added to the batch.
	return false, nil
}

// addSweeps tries to add sweeps to the batch. If this is the first sweep being
// added to the batch then it also sets the primary sweep ID. It returns if the
// sweeps were accepted to the batch.
func (b *batch) addSweeps(ctx context.Context, sweeps []*sweep) (bool, error) {
	done, err := b.scheduleNextCall()
	defer done()
	if err != nil {
		return false, err
	}

	// This must be a bug, so log a warning.
	if len(sweeps) == 0 {
		b.Warnf("An attempt to add zero sweeps.")

		return false, nil
	}

	// Track how many new and existing sweeps are among the sweeps.
	var numExisting, numNew int
	for _, s := range sweeps {
		existing, err := b.checkSweepToAdd(ctx, s)
		if err != nil {
			b.Infof("Failed to add sweep %v to batch %d: %v",
				s.outpoint, b.id, err)

			return false, nil
		}
		if existing {
			numExisting++
		} else {
			numNew++
		}
	}

	// Make sure the whole group is either new or existing. If this is not
	// the case, this might be a bug, so print a warning.
	if numExisting > 0 && numNew > 0 {
		b.Warnf("There are %d existing and %d new sweeps among the "+
			"group. They must not be mixed.", numExisting, numNew)

		return false, nil
	}

	// Make sure all the sweeps spend different outpoints.
	outpointsSet := make(map[wire.OutPoint]struct{}, len(sweeps))
	for _, s := range sweeps {
		if _, has := outpointsSet[s.outpoint]; has {
			b.Warnf("Multiple sweeps spend outpoint %v", s.outpoint)

			return false, nil
		}
		outpointsSet[s.outpoint] = struct{}{}
	}

	// Track if there is a presigned and a regular sweep.
	var addingPresigned, addingRegular bool
	for _, s := range sweeps {
		if s.presigned {
			addingPresigned = true
		} else {
			addingRegular = true
		}
	}
	if addingPresigned && addingRegular {
		b.Warnf("There are presigned and regular sweeps in the group")

		return false, nil
	}

	// If presigned mode is enabled, we should first presign the new version
	// of batch transaction. Also ensure that all the sweeps in the batch
	// use the same mode (presigned or regular).
	if addingPresigned {
		// Ensure that all the sweeps in the batch use presigned mode.
		for _, s := range b.sweeps {
			if !s.presigned {
				b.Warnf("Failed to add presigned sweep %x to "+
					"the batch, because the batch has "+
					"non-presigned sweep %x",
					sweeps[0].swapHash[:6], s.swapHash[:6])

				return false, nil
			}
		}

		switch {
		// We don't need to run checks if existing sweeps are updated.
		case numExisting == len(sweeps):

		// If new sweeps are added to the batch, we need to presign new
		// version of batch transaction.
		case len(b.sweeps) != 0:
			if err := b.presign(ctx, sweeps); err != nil {
				b.Warnf("Failed to add sweep %x to the batch, "+
					"because failed to presign new version"+
					" of batch tx: %v",
					sweeps[0].swapHash[:6], err)

				return false, nil
			}

		// If this is a new batch being formed, make sure we already
		// have a presigned transaction.
		default:
			const allowNonEmptyBatch = false
			err := b.ensurePresigned(
				ctx, sweeps, allowNonEmptyBatch,
			)
			if err != nil {
				b.Warnf("Failed to check signing of input %x,"+
					" this means that PresignSweepsGroup "+
					"was not called prior to AddSweep for"+
					" this input: %v",
					sweeps[0].swapHash[:6], err)

				return false, nil
			}
		}
	} else {
		// Ensure that all the sweeps in the batch don't use presigned.
		for _, s := range b.sweeps {
			if s.presigned {
				b.Warnf("failed to add a non-presigned sweep "+
					"%x to the batch, because the batch "+
					"has presigned sweep %x",
					sweeps[0].swapHash[:6], s.swapHash[:6])

				return false, nil
			}
		}
	}

	// Past this point we know that a new incoming sweep passes the
	// acceptance criteria and is now ready to be added to this batch.

	// For an existing group, update the sweeps in the batch.
	if numExisting == len(sweeps) {
		for _, s := range sweeps {
			oldSweep, ok := b.sweeps[s.outpoint]
			if !ok {
				return false, fmt.Errorf("sweep %v not found "+
					"in batch %d", s.outpoint, b.id)
			}

			// Preserve coopFailed value not to forget about
			// cooperative spending failure in this sweep.
			tmp := *s
			tmp.coopFailed = oldSweep.coopFailed

			// If the sweep was resumed from storage, and the swap
			// requested to sweep again, a new sweep notifier will
			// be created by the swap. By re-assigning to the
			// batch's sweep we make sure that everything, including
			// the notifier, is up to date.
			b.sweeps[s.outpoint] = tmp

			// If this is the primary sweep, we also need to update
			// the batch's confirmation target and fee rate.
			if b.primarySweepID == s.outpoint {
				b.cfg.batchConfTarget = s.confTarget
				b.rbfCache.SkipNextBump = true
			}

			// Update batch's fee rate to be greater than or equal
			// to minFeeRate of the sweep. Make sure batch's fee
			// rate does not decrease (otherwise it won't pass RBF
			// rules and won't be broadcasted) and that it is not
			// lower that minFeeRate of other sweeps (so it is
			// applied).
			if b.rbfCache.FeeRate < s.minFeeRate {
				b.Infof("Increasing feerate of the batch "+
					"from %v to %v", b.rbfCache.FeeRate,
					s.minFeeRate)
				b.rbfCache.FeeRate = s.minFeeRate
			}
		}

		return true, nil
	} else if numNew != len(sweeps) {
		// Sanity check: all the sweeps must be either existing or new.
		// We have checked this above, let's check here as well.
		return false, fmt.Errorf("bug in numExisting and numNew logic:"+
			" numExisting=%d, numNew=%d, len(sweeps)=%d, "+
			"len(b.sweeps)=%d", numExisting, numNew, len(sweeps),
			len(b.sweeps))
	}

	// Here is the code to add new sweeps to a batch.
	for _, s := range sweeps {
		// If this is the first sweep being added to the batch, make it
		// the primary sweep.
		if b.primarySweepID == zeroSweepID {
			b.primarySweepID = s.outpoint
			b.cfg.batchConfTarget = s.confTarget
			b.rbfCache.FeeRate = s.minFeeRate
			b.rbfCache.SkipNextBump = true

			// We also need to start the spend monitor for this new
			// primary sweep.
			err := b.monitorSpend(ctx, *s)
			if err != nil {
				return false, err
			}
		}

		// Make sure the sweep is not present in the batch. If it is
		// present, this is a bug, return an error to stop here.
		if _, has := b.sweeps[s.outpoint]; has {
			return false, fmt.Errorf("sweep %v is already present "+
				"in batch %d", s.outpoint, b.id)
		}

		// Add the sweep to the batch's sweeps.
		b.Infof("adding sweep %v, swap %x", s.outpoint, s.swapHash[:6])
		b.sweeps[s.outpoint] = *s

		// Update FeeRate. Max(s.minFeeRate) for all the sweeps of
		// the batch is the basis for fee bumps.
		if b.rbfCache.FeeRate < s.minFeeRate {
			b.Infof("Increasing feerate of the batch "+
				"from %v to %v", b.rbfCache.FeeRate,
				s.minFeeRate)
			b.rbfCache.FeeRate = s.minFeeRate
			b.rbfCache.SkipNextBump = true
		}

		if err := b.persistSweep(ctx, *s, false); err != nil {
			return true, err
		}
	}

	return true, nil
}

// sweepExists returns true if the batch contains the sweep with the given
// outpoint.
func (b *batch) sweepExists(outpoint wire.OutPoint) bool {
	done, err := b.scheduleNextCall()
	defer done()
	if err != nil {
		return false
	}

	_, ok := b.sweeps[outpoint]

	return ok
}

// Wait waits for the batch to gracefully stop.
func (b *batch) Wait() {
	b.Infof("Stopping")
	<-b.finished
}

// stillWaitingMsg is the format of the message printed if the batch is about
// to publish, but initial delay has not ended yet.
const stillWaitingMsg = "Skipping publishing, initial delay will end at " +
	"%v, now is %v."

// Run is the batch's main event loop.
func (b *batch) Run(ctx context.Context) error {
	runCtx, cancel := context.WithCancel(ctx)
	defer func() {
		cancel()
		close(b.stopping)

		// Make sure not to call b.wg.Wait from any other place to avoid
		// race condition between b.wg.Add(1) and b.wg.Wait().
		b.wg.Wait()
		close(b.finished)
	}()

	if b.muSig2SignSweep == nil && b.cfg.customMuSig2Signer == nil {
		return fmt.Errorf("no musig2 signer available")
	}
	if b.muSig2SignSweep != nil && b.cfg.customMuSig2Signer != nil {
		return fmt.Errorf("both musig2 signers provided")
	}

	// Cache clock variable.
	clock := b.cfg.clock
	startTime := clock.Now()

	blockChan, blockErrChan, err :=
		b.chainNotifier.RegisterBlockEpochNtfn(runCtx)
	if err != nil {
		return fmt.Errorf("block registration error: %w", err)
	}

	// Set currentHeight here, because it may be needed in monitorSpend.
	select {
	case b.currentHeight = <-blockChan:
		b.Debugf("initial height for the batch is %v", b.currentHeight)

	case <-runCtx.Done():
		return fmt.Errorf("context expired while waiting for current "+
			"height: %w", runCtx.Err())
	}

	// If a primary sweep exists we immediately start monitoring for its
	// spend.
	if b.primarySweepID != zeroSweepID {
		sweep := b.sweeps[b.primarySweepID]
		err := b.monitorSpend(runCtx, sweep)
		if err != nil {
			return fmt.Errorf("monitorSpend error: %w", err)
		}
	}

	// skipBefore is the time before which we skip batch publishing.
	// This is needed to facilitate better grouping of sweeps.
	// The value is set only if the batch has at least one sweep.
	// For batches loaded from DB initialDelay should be 0.
	var skipBefore *time.Time

	// initialDelayChan is a timer which fires upon initial delay end.
	// If initialDelay is set to 0, it will not trigger to avoid setting up
	// timerChan twice, which could lead to double publishing if
	// batchPublishDelay is also 0.
	var initialDelayChan <-chan time.Time

	// We use a timer in order to not publish new transactions at the same
	// time as the block epoch notification. This is done to prevent
	// unnecessary transaction publishments when a spend is detected on that
	// block. This timer starts after new block arrives (including the
	// current tip which we read from blockChan above) or when initialDelay
	// completes.
	timerChan := clock.TickAfter(b.cfg.batchPublishDelay)

	b.Infof("started, primary %s, total sweeps %d, state: %d",
		b.primarySweepID, len(b.sweeps), b.state)

	for {
		// If the batch is not empty, find earliest initialDelay.
		var totalSweptAmt btcutil.Amount
		for _, sweep := range b.sweeps {
			totalSweptAmt += sweep.value
		}

		skipBeforeUpdated := false
		if totalSweptAmt != 0 {
			fast := false
			initialDelay, err := b.cfg.initialDelayProvider(
				ctx, len(b.sweeps), totalSweptAmt, fast,
			)
			if err != nil {
				b.Warnf("InitialDelayProvider failed: %v. We "+
					"publish this batch without a delay.",
					err)
				initialDelay = 0
			}
			if initialDelay < 0 {
				b.Warnf("Negative delay: %v. We publish this "+
					"batch without a delay.", initialDelay)
				initialDelay = 0
			}
			delayStop := startTime.Add(initialDelay)
			if skipBefore == nil || delayStop.Before(*skipBefore) {
				skipBefore = &delayStop
				skipBeforeUpdated = true
			}
		}

		// Create new timer only if the value of skipBefore was updated.
		// Don't create the timer if the delay is <= 0 to avoid double
		// publishing if batchPublishDelay is also 0.
		if skipBeforeUpdated {
			delay := skipBefore.Sub(clock.Now())
			if delay > 0 {
				initialDelayChan = clock.TickAfter(delay)
			}
		}

		select {
		case <-b.callEnter:
			<-b.callLeave

		// blockChan provides immediately the current tip.
		case height := <-blockChan:
			b.Debugf("received block %v", height)

			// Set the timer to publish the batch transaction after
			// the configured delay.
			timerChan = clock.TickAfter(b.cfg.batchPublishDelay)
			b.currentHeight = height

		case <-initialDelayChan:
			b.Debugf("initial delay of duration %v has ended",
				clock.Now().Sub(startTime))

			// Set the timer to publish the batch transaction after
			// the configured delay.
			timerChan = clock.TickAfter(b.cfg.batchPublishDelay)

		case <-timerChan:
			// Check that batch is still open.
			if b.state != Open {
				b.Debugf("Skipping publishing, because "+
					"the batch is not open (%v).", b.state)
				continue
			}

			if skipBefore == nil {
				b.Debugf("Skipping publishing, because " +
					"the batch is empty.")
				continue
			}

			// If the batch became urgent, skipBefore is set to now.
			if b.isUrgent(*skipBefore) {
				*skipBefore = clock.Now()
			}

			// Check that the initial delay has ended. We have also
			// batchPublishDelay on top of initialDelay, so if
			// initialDelayChan has just fired, this check passes.
			now := clock.Now()
			if skipBefore.After(now) {
				b.Debugf(stillWaitingMsg, *skipBefore, now)
				continue
			}

			// Update feerate of sweeps. This is normally done by
			// AddSweep, but it may not be called after the sweep
			// is confirmed, but fresh feerate is still needed to
			// keep publishing in case of reorg.
			b.updateFeeRate(ctx)

			err := b.publish(ctx)
			if err != nil {
				return fmt.Errorf("publish error: %w", err)
			}

		case spend := <-b.spendChan:
			err := b.handleSpend(runCtx, spend.SpendingTx)
			if err != nil {
				return fmt.Errorf("handleSpend error: %w", err)
			}

		case err := <-b.spendErrChan:
			b.writeToSpendErrChan(ctx, err)

			return fmt.Errorf("spend notifier failed: %w", err)

		case conf := <-b.confChan:
			if err := b.handleConf(runCtx, conf); err != nil {
				return fmt.Errorf("handleConf error: %w", err)
			}

			return nil

		// A re-org has been detected. We set the batch state back to
		// open since our batch transaction is no longer present in any
		// block. We can accept more sweeps and try to publish.
		case <-b.reorgChan:
			b.state = Open
			b.Warnf("reorg detected, batch is able to " +
				"accept new sweeps")

		case testReq := <-b.testReqs:
			testReq.handler()
			close(testReq.quit)

		case err := <-blockErrChan:
			return fmt.Errorf("blocks monitoring error: %w", err)

		case err := <-b.errChan:
			return fmt.Errorf("error with the batch: %w", err)

		case <-runCtx.Done():
			return fmt.Errorf("batch context expired: %w",
				runCtx.Err())
		}
	}
}

// updateFeeRate gets fresh values of minFeeRate for sweeps and updates the
// feerate of the batch if needed. This method must be called from event loop.
func (b *batch) updateFeeRate(ctx context.Context) {
	for outpoint, s := range b.sweeps {
		minFeeRate, err := minimumSweepFeeRate(
			ctx, b.cfg.customFeeRate, b.wallet,
			s.swapHash, s.outpoint, s.confTarget,
		)
		if err != nil {
			b.Warnf("failed to determine feerate for sweep %v of "+
				"swap %x, confTarget %d: %w", s.outpoint,
				s.swapHash[:6], s.confTarget, err)
			continue
		}

		if minFeeRate <= s.minFeeRate {
			continue
		}

		b.Infof("Increasing feerate of sweep %v of swap %x from %v "+
			"to %v", s.outpoint, s.swapHash[:6], s.minFeeRate,
			minFeeRate)
		s.minFeeRate = minFeeRate
		b.sweeps[outpoint] = s

		if s.minFeeRate <= b.rbfCache.FeeRate {
			continue
		}

		b.Infof("Increasing feerate of the batch from %v to %v",
			b.rbfCache.FeeRate, s.minFeeRate)
		b.rbfCache.FeeRate = s.minFeeRate
	}
}

// testRunInEventLoop runs a function in the event loop blocking until
// the function returns. For unit tests only!
func (b *batch) testRunInEventLoop(ctx context.Context, handler func()) {
	// If the event loop is finished, run the function.
	select {
	case <-b.stopping:
		handler()

		return
	default:
	}

	quit := make(chan struct{})
	req := &testRequest{
		handler: handler,
		quit:    quit,
	}

	select {
	case b.testReqs <- req:
	case <-ctx.Done():
		return
	}

	select {
	case <-quit:
	case <-ctx.Done():
	}
}

// timeout returns minimum timeout as block height among sweeps of the batch.
// If the batch is empty, return -1.
func (b *batch) timeout() int32 {
	// Find minimum among sweeps' timeouts.
	minTimeout := int32(-1)
	for _, sweep := range b.sweeps {
		if minTimeout == -1 || minTimeout > sweep.timeout {
			minTimeout = sweep.timeout
		}
	}

	return minTimeout
}

// isUrgent checks if the batch became urgent. This is determined by comparing
// the remaining number of blocks until timeout to the initial delay remained,
// given one block is 10 minutes.
func (b *batch) isUrgent(skipBefore time.Time) bool {
	timeout := b.timeout()
	if timeout <= 0 {
		// This may happen if the batch is empty or if SweepInfo.Timeout
		// is not set, may be possible in tests or if there is a bug.
		b.Warnf("Method timeout() returned %v. Number of "+
			"sweeps: %d. It may be an empty batch.",
			timeout, len(b.sweeps))
		return false
	}

	if b.currentHeight == 0 {
		// currentHeight is not initiated yet.
		return false
	}

	blocksToTimeout := timeout - b.currentHeight
	const blockTime = 10 * time.Minute
	timeBank := time.Duration(blocksToTimeout) * blockTime

	// We want to have at least 2x as much time to be safe.
	const safetyFactor = 2
	remainingWaiting := skipBefore.Sub(b.cfg.clock.Now())

	if timeBank >= safetyFactor*remainingWaiting {
		// There is enough time, keep waiting.
		return false
	}

	b.Debugf("cancelling waiting for urgent sweep (timeBank is %v, "+
		"remainingWaiting is %v)", timeBank, remainingWaiting)

	// Signal to the caller to cancel initialDelay.
	return true
}

// isPresigned returns if the batch uses presigned mode. Currently presigned and
// non-presigned sweeps never appear in the same batch. Fails if the batch is
// empty or contains both presigned and regular sweeps.
func (b *batch) isPresigned() (bool, error) {
	var (
		hasPresigned bool
		hasRegular   bool
	)

	for _, sweep := range b.sweeps {
		if sweep.presigned {
			hasPresigned = true
		} else {
			hasRegular = true
		}
	}

	switch {
	case hasPresigned && !hasRegular:
		return true, nil

	case !hasPresigned && hasRegular:
		return false, nil

	case hasPresigned && hasRegular:
		return false, fmt.Errorf("the batch has both presigned and " +
			"non-presigned sweeps")

	default:
		return false, fmt.Errorf("the batch is empty")
	}
}

// publish creates and publishes the latest batch transaction to the network.
func (b *batch) publish(ctx context.Context) error {
	var (
		err         error
		fee         btcutil.Amount
		signSuccess bool
	)

	if len(b.sweeps) == 0 {
		b.Debugf("skipping publish: no sweeps in the batch")

		return nil
	}

	// Run the RBF rate update.
	err = b.updateRbfRate(ctx)
	if err != nil {
		return err
	}

	// logPublishError is a function which logs publish errors.
	logPublishError := func(errMsg string, err error) {
		b.publishErrorHandler(err, errMsg, b.log())
	}

	// Determine if we should use presigned mode for the batch.
	presigned, err := b.isPresigned()
	if err != nil {
		return fmt.Errorf("failed to determine if the batch %d uses "+
			"presigned mode: %w", b.id, err)
	}

	if presigned {
		fee, err, signSuccess = b.publishPresigned(ctx)
	} else {
		fee, err, signSuccess = b.publishMixedBatch(ctx)
	}

	if err != nil {
		if signSuccess {
			logPublishError("publish error", err)

			// Publishing error is expected: "insufficient fee" and
			// "output already spent". Don't return the error here
			// not to break the main loop of the sweep batch.
			return nil
		} else {
			logPublishError("signing error", err)

			// Signing error is not expected, because we have
			// non-cooperative method of signing which should
			// always succeed.
			return err
		}
	}

	b.Infof("published, total sweeps: %v, fees: %v", len(b.sweeps), fee)
	for _, sweep := range b.sweeps {
		b.Infof("published sweep %x, value: %v",
			sweep.swapHash[:6], sweep.value)
	}

	return b.persist(ctx)
}

// createPsbt creates serialized PSBT and prevOuts map from unsignedTx and
// the list of sweeps.
func (b *batch) createPsbt(unsignedTx *wire.MsgTx, sweeps []sweep) ([]byte,
	map[wire.OutPoint]*wire.TxOut, error) {

	// Create PSBT packet object.
	packet, err := psbt.NewFromUnsignedTx(unsignedTx)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to create PSBT: %w", err)
	}

	// Sanity check: the number of inputs in PSBT must be equal to the
	// number of sweeps.
	if len(packet.Inputs) != len(sweeps) {
		return nil, nil, fmt.Errorf("invalid number of packet inputs")
	}

	// Create prevOuts map.
	prevOuts := make(map[wire.OutPoint]*wire.TxOut, len(sweeps))

	// Fill input info in PSBT and prevOuts.
	for i, sweep := range sweeps {
		txOut := &wire.TxOut{
			Value:    int64(sweep.value),
			PkScript: sweep.htlc.PkScript,
		}

		prevOuts[sweep.outpoint] = txOut
		packet.Inputs[i].WitnessUtxo = txOut
	}

	// Serialize PSBT.
	var psbtBuf bytes.Buffer
	err = packet.Serialize(&psbtBuf)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to serialize PSBT: %w", err)
	}

	return psbtBuf.Bytes(), prevOuts, nil
}

// constructUnsignedTx creates unsigned tx from the sweeps, paying to the addr.
// It also returns absolute fee (from weight and clamped). The main output is
// the first output of the transaction, followed by an optional list of change
// outputs. If the main output value is below dust limit this function will
// return an error.
func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
	currentHeight int32, feeRate chainfee.SatPerKWeight,
	minRelayFeeRate chainfee.SatPerKWeight) (*wire.MsgTx,
	lntypes.WeightUnit, btcutil.Amount, btcutil.Amount, error) {

	// Sanity check, there should be at least 1 sweep in this batch.
	if len(sweeps) == 0 {
		return nil, 0, 0, 0, fmt.Errorf("no sweeps in batch")
	}

	// Create the batch transaction.
	batchTx := &wire.MsgTx{
		Version:  2,
		LockTime: uint32(currentHeight),
	}

	// Consolidate change outputs with identical pkscript.
	changeOutputs := make(map[string]*wire.TxOut)
	for _, s := range sweeps {
		if s.change == nil {
			continue
		}

		stringPkScript := string(s.change.PkScript)
		if _, has := changeOutputs[stringPkScript]; has {
			changeOutputs[stringPkScript].Value += s.change.Value
			continue
		}

		changeOutputs[stringPkScript] = &wire.TxOut{
			Value:    s.change.Value,
			PkScript: s.change.PkScript,
		}
	}

	// Add transaction inputs and estimate its weight.
	var weightEstimate input.TxWeightEstimator
	for _, sweep := range sweeps {
		if sweep.nonCoopHint || sweep.coopFailed {
			// Non-cooperative sweep.
			batchTx.AddTxIn(&wire.TxIn{
				PreviousOutPoint: sweep.outpoint,
				Sequence:         sweep.htlc.SuccessSequence(),
			})

			err := sweep.htlcSuccessEstimator(&weightEstimate)
			if err != nil {
				return nil, 0, 0, 0, fmt.Errorf("sweep."+
					"htlcSuccessEstimator failed: %w", err)
			}
		} else {
			// Cooperative sweep.
			batchTx.AddTxIn(&wire.TxIn{
				PreviousOutPoint: sweep.outpoint,
			})

			weightEstimate.AddTaprootKeySpendInput(
				txscript.SigHashDefault,
			)
		}
	}

	// Convert the destination address to pkScript.
	batchPkScript, err := txscript.PayToAddrScript(address)
	if err != nil {
		return nil, 0, 0, 0, fmt.Errorf("txscript.PayToAddrScript "+
			"failed: %w", err)
	}
	if len(batchPkScript) == 0 {
		return nil, 0, 0, 0, fmt.Errorf("txscript.PayToAddrScript " +
			"returned an empty pkScript")
	}

	// Add the output to weight estimates.
	err = sweeppkg.AddOutputEstimate(&weightEstimate, address)
	if err != nil {
		return nil, 0, 0, 0, fmt.Errorf("sweep.AddOutputEstimate "+
			"failed: %w", err)
	}

	// Add the optional change outputs to weight estimates.
	for _, o := range changeOutputs {
		weightEstimate.AddOutput(o.PkScript)
	}

	// Keep track of the total amount this batch is sweeping back.
	batchAmt := btcutil.Amount(0)
	for _, sweep := range sweeps {
		batchAmt += sweep.value
	}

	// Find weight and fee.
	weight := weightEstimate.Weight()
	feeForWeight := feeRate.FeeForWeight(weight)

	// Fee can be rounded towards zero, leading to actual feeRate being
	// slightly lower than the requested value. Increase the fee if this is
	// the case.
	if chainfee.NewSatPerKWeight(feeForWeight, weight) < feeRate {
		feeForWeight++
	}

	// Add the batch transaction output, which excludes the fees paid to
	// miners. Reduce the amount by the sum of change outputs, if any.
	var sumChange int64
	for _, change := range changeOutputs {
		sumChange += change.Value
	}

	// Ensure that the batch amount is greater than the sum of change.
	if batchAmt <= btcutil.Amount(sumChange) {
		return nil, 0, 0, 0, fmt.Errorf("batch amount %v is <= the "+
			"sum of change outputs %v", batchAmt,
			btcutil.Amount(sumChange))
	}

	// Clamp the calculated fee to the max allowed fee amount for the batch.
	fee, _, err := utils.ClampSweepFee(
		feeForWeight, batchAmt-btcutil.Amount(sumChange),
		utils.MaxFeeToAmountRatio, minRelayFeeRate, weight,
	)
	if err != nil {
		return nil, 0, 0, 0, fmt.Errorf("failed to clamp batch "+
			"fee: %w", err)
	}

	// Ensure that batch amount is equal or exceeds the sum of change
	// outputs and the fee, and that it is also greater than dust limit
	// for the main output.
	dustLimit := utils.DustLimitForPkScript(batchPkScript)
	if fee+btcutil.Amount(sumChange)+dustLimit > batchAmt {
		return nil, 0, 0, 0, fmt.Errorf("batch amount %v is < the "+
			"sum of change outputs %v plus fee %v and dust "+
			"limit %v", batchAmt, btcutil.Amount(sumChange),
			fee, dustLimit)
	}

	// Add the main output first.
	batchTx.AddTxOut(&wire.TxOut{
		PkScript: batchPkScript,
		Value:    int64(batchAmt-fee) - sumChange,
	})
	// Then add change outputs. Sort the keys first to make tests
	// deterministic.
	sortedChangeOutputs := make([]*wire.TxOut, 0, len(changeOutputs))
	for _, output := range changeOutputs {
		sortedChangeOutputs = append(sortedChangeOutputs, output)
	}

	// Sort the keys
	sort.Slice(sortedChangeOutputs, func(i, j int) bool {
		return utils.Bip69Less(
			sortedChangeOutputs[i], sortedChangeOutputs[j],
		)
	})

	// Add change outputs orderly.
	for _, output := range sortedChangeOutputs {
		batchTx.AddTxOut(&wire.TxOut{
			PkScript: output.PkScript,
			Value:    output.Value,
		})
	}

	// Check that for each swap, inputs exceed the change outputs.
	if len(changeOutputs) != 0 {
		swap2Inputs := make(map[lntypes.Hash]btcutil.Amount)
		swap2Change := make(map[lntypes.Hash]btcutil.Amount)
		for _, sweep := range sweeps {
			swap2Inputs[sweep.swapHash] += sweep.value
			if sweep.change != nil {
				swap2Change[sweep.swapHash] +=
					btcutil.Amount(sweep.change.Value)
			}
		}

		for swapHash, inputs := range swap2Inputs {
			change := swap2Change[swapHash]
			if inputs <= change {
				return nil, 0, 0, 0, fmt.Errorf(""+
					"inputs %v <= change %v for swap %x",
					inputs, change, swapHash[:6])
			}
		}
	}

	// Ensure that each output is above dust limit.
	for _, txOut := range batchTx.TxOut {
		dustLimit = utils.DustLimitForPkScript(txOut.PkScript)
		if btcutil.Amount(txOut.Value) < dustLimit {
			return nil, 0, 0, 0, fmt.Errorf("output %v is below "+
				"dust limit %v", btcutil.Amount(txOut.Value),
				dustLimit)
		}
	}

	return batchTx, weight, feeForWeight, fee, nil
}

// publishMixedBatch constructs and publishes a batch transaction that can
// include sweeps spent both cooperatively and non-cooperatively. If a sweep is
// marked with nonCoopHint or coopFailed flags, it is spent non-cooperatively.
// If a cooperative sweep fails to sign cooperatively, the whole transaction
// is re-signed again, with this sweep signing non-cooperatively. This process
// is optimized, trying to detect all non-cooperative sweeps in one round. The
// function returns the absolute fee. The last result of the function indicates
// if signing succeeded.
func (b *batch) publishMixedBatch(ctx context.Context) (btcutil.Amount, error,
	bool) {

	// Sanity check, there should be at least 1 sweep in this batch.
	if len(b.sweeps) == 0 {
		return 0, fmt.Errorf("no sweeps in batch"), false
	}

	// Append this sweep to an array of sweeps. This is needed to keep the
	// order of sweeps stored, as iterating the sweeps map does not
	// guarantee same order.
	sweeps := make([]sweep, 0, len(b.sweeps))
	for _, sweep := range b.sweeps {
		sweeps = append(sweeps, sweep)
	}

	// Determine if an external address is used.
	addrOverride := false
	for _, sweep := range sweeps {
		if sweep.isExternalAddr {
			addrOverride = true
		}
	}

	// Find destination address.
	var address btcutil.Address
	if addrOverride {
		// Sanity check, there should be exactly 1 sweep in this batch.
		if len(sweeps) != 1 {
			return 0, fmt.Errorf("external address sweep batched " +
				"with other sweeps"), false
		}

		address = sweeps[0].destAddr
	} else {
		var err error
		address, err = b.getBatchDestAddr(ctx)
		if err != nil {
			return 0, err, false
		}
	}

	// Each iteration of this loop is one attempt to sign the transaction
	// cooperatively. We try cooperative signing only for the sweeps not
	// known in advance to be non-cooperative (nonCoopHint) and not failed
	// to sign cooperatively in previous rounds (coopFailed). If any of them
	// fails, the sweep is excluded from all following rounds and another
	// round is attempted. Otherwise, the cycle completes and we sign the
	// remaining sweeps non-cooperatively.
	var (
		tx              *wire.MsgTx
		weight          lntypes.WeightUnit
		feeForWeight    btcutil.Amount
		fee             btcutil.Amount
		minRelayFeeRate chainfee.SatPerKWeight
		coopInputs      int
	)
	minRelayFeeRate, err := b.wallet.MinRelayFee(ctx)
	if err != nil {
		return 0, fmt.Errorf("failed to get min relay fee: %w", err),
			false
	}
	for attempt := 1; ; attempt++ {
		b.Infof("Attempt %d of collecting cooperative signatures.",
			attempt)

		// Construct unsigned batch transaction.
		var err error
		tx, weight, feeForWeight, fee, err = constructUnsignedTx(
			sweeps, address, b.currentHeight, b.rbfCache.FeeRate,
			minRelayFeeRate,
		)
		if err != nil {
			return 0, fmt.Errorf("failed to construct tx: %w", err),
				false
		}

		// Create PSBT and prevOutsMap.
		psbtBytes, prevOutsMap, err := b.createPsbt(tx, sweeps)
		if err != nil {
			return 0, fmt.Errorf("createPsbt failed: %w", err),
				false
		}

		// Keep track if any new sweep failed to sign cooperatively.
		newCoopFailures := false

		// Try to sign all cooperative sweeps first.
		coopInputs = 0
		for i, sweep := range sweeps {
			// Skip non-cooperative sweeps.
			if sweep.nonCoopHint || sweep.coopFailed {
				continue
			}

			// Try to sign the sweep cooperatively.
			finalSig, err := b.musig2sign(
				ctx, i, sweep, tx, prevOutsMap, psbtBytes,
			)
			if err != nil {
				b.Infof("cooperative signing failed for "+
					"sweep %x: %v", sweep.swapHash[:6], err)

				// Set coopFailed flag for this sweep in all the
				// places we store the sweep.
				sweep.coopFailed = true
				sweeps[i] = sweep
				b.sweeps[sweep.outpoint] = sweep

				// Update newCoopFailures to know if we need
				// another attempt of cooperative signing.
				newCoopFailures = true
			} else {
				// Put the signature to witness of the input.
				tx.TxIn[i].Witness = wire.TxWitness{finalSig}
				coopInputs++
			}
		}

		// If there was any failure of cooperative signing, we need to
		// update weight estimates (since non-cooperative signing has
		// larger witness) and hence update the whole transaction and
		// all the signatures. Otherwise, we complete cooperative part.
		if !newCoopFailures {
			break
		}
	}

	// Calculate the expected number of non-cooperative sweeps.
	nonCoopInputs := len(sweeps) - coopInputs

	// Now sign the remaining sweeps' inputs non-cooperatively.
	// For that, first collect sign descriptors for the signatures.
	// Also collect prevOuts for all inputs.
	signDescs := make([]*lndclient.SignDescriptor, 0, nonCoopInputs)
	prevOutsList := make([]*wire.TxOut, 0, len(sweeps))
	for i, sweep := range sweeps {
		// Create and store the previous outpoint for this sweep.
		prevOut := &wire.TxOut{
			Value:    int64(sweep.value),
			PkScript: sweep.htlc.PkScript,
		}
		prevOutsList = append(prevOutsList, prevOut)

		// Skip cooperative sweeps.
		if !sweep.nonCoopHint && !sweep.coopFailed {
			continue
		}

		key, err := btcec.ParsePubKey(
			sweep.htlcKeys.ReceiverScriptKey[:],
		)
		if err != nil {
			return 0, fmt.Errorf("btcec.ParsePubKey failed: %w",
				err), false
		}

		// Create and store the sign descriptor for this sweep.
		signDesc := lndclient.SignDescriptor{
			WitnessScript: sweep.htlc.SuccessScript(),
			Output:        prevOut,
			HashType:      sweep.htlc.SigHash(),
			InputIndex:    i,
			KeyDesc: keychain.KeyDescriptor{
				PubKey: key,
			},
		}

		if sweep.htlc.Version == swap.HtlcV3 {
			signDesc.SignMethod = input.TaprootScriptSpendSignMethod
		}

		signDescs = append(signDescs, &signDesc)
	}

	// Sanity checks.
	if len(signDescs) != nonCoopInputs {
		// This must not happen by construction.
		return 0, fmt.Errorf("unexpected size of signDescs: %d != %d",
			len(signDescs), nonCoopInputs), false
	}
	if len(prevOutsList) != len(sweeps) {
		// This must not happen by construction.
		return 0, fmt.Errorf("unexpected size of prevOutsList: "+
			"%d != %d", len(prevOutsList), len(sweeps)), false
	}

	var rawSigs [][]byte
	if nonCoopInputs > 0 {
		// Produce the signatures for our inputs using sign descriptors.
		var err error
		rawSigs, err = b.signerClient.SignOutputRaw(
			ctx, tx, signDescs, prevOutsList,
		)
		if err != nil {
			return 0, fmt.Errorf("signerClient.SignOutputRaw "+
				"failed: %w", err), false
		}
	}

	// Sanity checks.
	if len(rawSigs) != nonCoopInputs {
		// This must not happen by construction.
		return 0, fmt.Errorf("unexpected size of rawSigs: %d != %d",
			len(rawSigs), nonCoopInputs), false
	}

	// Generate success witnesses for non-cooperative sweeps.
	sigIndex := 0
	for i, sweep := range sweeps {
		// Skip cooperative sweeps.
		if !sweep.nonCoopHint && !sweep.coopFailed {
			continue
		}

		witness, err := sweep.htlc.GenSuccessWitness(
			rawSigs[sigIndex], sweep.preimage,
		)
		if err != nil {
			return 0, fmt.Errorf("sweep.htlc.GenSuccessWitness "+
				"failed: %w", err), false
		}
		sigIndex++

		// Add the success witness to our batch transaction's inputs.
		tx.TxIn[i].Witness = witness
	}

	// Log transaction's details.
	var coopHexs, nonCoopHexs []string
	for _, sweep := range sweeps {
		swapHex := fmt.Sprintf("%x", sweep.swapHash[:6])
		if sweep.nonCoopHint || sweep.coopFailed {
			nonCoopHexs = append(nonCoopHexs, swapHex)
		} else {
			coopHexs = append(coopHexs, swapHex)
		}
	}
	txHash := tx.TxHash()
	b.Infof("attempting to publish batch tx=%v with feerate=%v, "+
		"weight=%v, feeForWeight=%v, fee=%v, sweeps=%d, "+
		"%d cooperative: (%s) and %d non-cooperative (%s), destAddr=%s",
		txHash, b.rbfCache.FeeRate, weight, feeForWeight, fee,
		len(tx.TxIn), coopInputs, strings.Join(coopHexs, ", "),
		nonCoopInputs, strings.Join(nonCoopHexs, ", "), address)

	b.debugLogTx("serialized batch", tx)

	// Make sure tx weight matches the expected value.
	realWeight := lntypes.WeightUnit(
		blockchain.GetTransactionWeight(btcutil.NewTx(tx)),
	)
	if realWeight != weight {
		b.Warnf("actual weight of tx %v is %v, estimated as %d",
			txHash, realWeight, weight)
	}

	// Publish the transaction.
	err = b.wallet.PublishTransaction(
		ctx, tx, b.cfg.txLabeler(b.id),
	)
	if err != nil {
		return 0, fmt.Errorf("publishing tx failed: %w", err), true
	}

	// Store the batch transaction's txid and pkScript, for monitoring
	// purposes.
	b.batchTxid = &txHash
	b.batchPkScript = tx.TxOut[0].PkScript

	return fee, nil, true
}

func (b *batch) debugLogTx(msg string, tx *wire.MsgTx) {
	// Serialize the transaction and convert to hex string.
	buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize()))
	if err := tx.Serialize(buf); err != nil {
		b.Errorf("failed to serialize tx for debug log: %v", err)
		return
	}

	b.Debugf("%s: %s", msg, hex.EncodeToString(buf.Bytes()))
}

// musig2sign signs one sweep using musig2.
func (b *batch) musig2sign(ctx context.Context, inputIndex int, sweep sweep,
	unsignedTx *wire.MsgTx, prevOuts map[wire.OutPoint]*wire.TxOut,
	psbt []byte) ([]byte, error) {

	prevOutputFetcher := txscript.NewMultiPrevOutFetcher(prevOuts)

	sigHashes := txscript.NewTxSigHashes(unsignedTx, prevOutputFetcher)

	sigHash, err := txscript.CalcTaprootSignatureHash(
		sigHashes, txscript.SigHashDefault, unsignedTx, inputIndex,
		prevOutputFetcher,
	)
	if err != nil {
		return nil, err
	}

	var (
		signers       [][]byte
		muSig2Version input.MuSig2Version
	)

	// Depending on the MuSig2 version we either pass 32 byte
	// Schnorr public keys or normal 33 byte public keys.
	if sweep.protocolVersion >= loopdb.ProtocolVersionMuSig2 {
		muSig2Version = input.MuSig2Version100RC2
		signers = [][]byte{
			sweep.htlcKeys.SenderInternalPubKey[:],
			sweep.htlcKeys.ReceiverInternalPubKey[:],
		}
	} else {
		muSig2Version = input.MuSig2Version040
		signers = [][]byte{
			sweep.htlcKeys.SenderInternalPubKey[1:],
			sweep.htlcKeys.ReceiverInternalPubKey[1:],
		}
	}

	htlcScript, ok := sweep.htlc.HtlcScript.(*swap.HtlcScriptV3)
	if !ok {
		return nil, fmt.Errorf("invalid htlc script version")
	}

	var digest [32]byte
	copy(digest[:], sigHash)

	// If a custom signer is installed, use it instead of b.signerClient
	// and b.muSig2SignSweep.
	if b.cfg.customMuSig2Signer != nil {
		// Produce a signature.
		finalSig, err := b.cfg.customMuSig2Signer(
			ctx, muSig2Version, sweep.swapHash,
			htlcScript.RootHash, digest,
		)
		if err != nil {
			return nil, fmt.Errorf("customMuSig2Signer failed: %w",
				err)
		}

		// To be sure that we're good, parse and validate that the
		// combined signature is indeed valid for the sig hash and the
		// internal pubkey.
		err = b.verifySchnorrSig(
			htlcScript.TaprootKey, sigHash, finalSig,
		)
		if err != nil {
			return nil, fmt.Errorf("verifySchnorrSig failed: %w",
				err)
		}

		return finalSig, nil
	}

	// Now we're creating a local MuSig2 session using the receiver key's
	// key locator and the htlc's root hash.
	keyLocator := &sweep.htlcKeys.ClientScriptKeyLocator
	musig2SessionInfo, err := b.signerClient.MuSig2CreateSession(
		ctx, muSig2Version, keyLocator, signers,
		lndclient.MuSig2TaprootTweakOpt(htlcScript.RootHash[:], false),
	)
	if err != nil {
		return nil, fmt.Errorf("signerClient.MuSig2CreateSession "+
			"failed: %w", err)
	}

	// With the session active, we can now send the server our
	// public nonce and the sig hash, so that it can create it's own
	// MuSig2 session and return the server side nonce and partial
	// signature.
	serverNonce, serverSig, err := b.muSig2SignSweep(
		ctx, sweep.protocolVersion, sweep.swapHash,
		sweep.swapInvoicePaymentAddr,
		musig2SessionInfo.PublicNonce[:], psbt, prevOuts,
	)
	if err != nil {
		return nil, err
	}

	var serverPublicNonce [musig2.PubNonceSize]byte
	copy(serverPublicNonce[:], serverNonce)

	// Register the server's nonce before attempting to create our
	// partial signature.
	haveAllNonces, err := b.signerClient.MuSig2RegisterNonces(
		ctx, musig2SessionInfo.SessionID,
		[][musig2.PubNonceSize]byte{serverPublicNonce},
	)
	if err != nil {
		return nil, err
	}

	// Sanity check that we have all the nonces.
	if !haveAllNonces {
		return nil, fmt.Errorf("invalid MuSig2 session: " +
			"nonces missing")
	}

	// Since our MuSig2 session has all nonces, we can now create
	// the local partial signature by signing the sig hash.
	_, err = b.signerClient.MuSig2Sign(
		ctx, musig2SessionInfo.SessionID, digest, false,
	)
	if err != nil {
		return nil, err
	}

	// Now combine the partial signatures to use the final combined
	// signature in the sweep transaction's witness.
	haveAllSigs, finalSig, err := b.signerClient.MuSig2CombineSig(
		ctx, musig2SessionInfo.SessionID, [][]byte{serverSig},
	)
	if err != nil {
		return nil, err
	}

	if !haveAllSigs {
		return nil, fmt.Errorf("failed to combine signatures")
	}

	// To be sure that we're good, parse and validate that the
	// combined signature is indeed valid for the sig hash and the
	// internal pubkey.
	err = b.verifySchnorrSig(htlcScript.TaprootKey, sigHash, finalSig)
	if err != nil {
		return nil, err
	}

	return finalSig, nil
}

// updateRbfRate updates the fee rate we should use for the new batch
// transaction. This fee rate does not guarantee RBF success, but the continuous
// increase leads to an eventual successful RBF replacement.
func (b *batch) updateRbfRate(ctx context.Context) error {
	// If the feeRate is unset then we never published before, so we
	// retrieve the fee estimate from our wallet.
	if b.rbfCache.FeeRate == 0 {
		// We set minFeeRate in each sweep, so fee rate is expected to
		// be initiated here.
		b.Warnf("rbfCache.FeeRate is 0, which must not happen.")

		if b.cfg.batchConfTarget == 0 {
			b.Warnf("updateRbfRate called with zero " +
				"batchConfTarget")
		}

		b.Infof("initializing rbf fee rate for conf target=%v",
			b.cfg.batchConfTarget)

		rate, err := b.wallet.EstimateFeeRate(
			ctx, b.cfg.batchConfTarget,
		)
		if err != nil {
			return err
		}

		// Set the initial value for our fee rate.
		b.rbfCache.FeeRate = rate
	} else if noBumping := b.cfg.customFeeRate != nil; !noBumping {
		if b.rbfCache.SkipNextBump {
			// Skip fee bumping, unset the flag, to bump next time.
			b.rbfCache.SkipNextBump = false
		} else {
			// Bump the fee rate by the configured step.
			b.rbfCache.FeeRate += defaultFeeRateStep
		}
	}

	b.rbfCache.LastHeight = b.currentHeight

	return b.persist(ctx)
}

// monitorSpend monitors the primary sweep's outpoint for spends. The reason we
// monitor the primary sweep's outpoint is because the primary sweep was the
// first sweep that entered this batch, therefore it is present in all the
// versions of the batch transaction. This means that even if an older version
// of the batch transaction gets confirmed, due to the uncertainty of RBF
// replacements and network propagation, we can always detect the transaction.
func (b *batch) monitorSpend(ctx context.Context, primarySweep sweep) error {
	if b.spendChan != nil || b.spendErrChan != nil || b.reorgChan != nil {
		return fmt.Errorf("an attempt to run monitorSpend multiple " +
			"times per batch")
	}

	reorgChan := make(chan struct{}, 1)

	spendChan, spendErrChan, err := b.chainNotifier.RegisterSpendNtfn(
		ctx, &primarySweep.outpoint, primarySweep.htlc.PkScript,
		primarySweep.initiationHeight,
		lndclient.WithReOrgChan(reorgChan),
	)
	if err != nil {
		return fmt.Errorf("failed to register spend notifier for "+
			"primary sweep %v, pkscript %x, height %d: %w",
			primarySweep.outpoint, primarySweep.htlc.PkScript,
			primarySweep.initiationHeight, err)
	}

	b.Infof("monitoring spend for outpoint %s",
		primarySweep.outpoint.String())

	// This is safe to do as we always call monitorSpend from the event
	// loop's goroutine.
	b.spendChan = spendChan
	b.spendErrChan = spendErrChan
	b.reorgChan = reorgChan

	return nil
}

// monitorConfirmations monitors the batch transaction for confirmations.
func (b *batch) monitorConfirmations(ctx context.Context) error {
	// Find initiationHeight.
	primarySweep, ok := b.sweeps[b.primarySweepID]
	if !ok {
		return fmt.Errorf("can't find primarySweep")
	}

	confCtx, cancel := context.WithCancel(ctx)

	confChan, errChan, err := b.chainNotifier.RegisterConfirmationsNtfn(
		confCtx, b.batchTxid, b.batchPkScript, batchConfHeight,
		primarySweep.initiationHeight,
	)
	if err != nil {
		cancel()
		return err
	}

	b.wg.Add(1)
	go func() {
		defer cancel()
		defer b.wg.Done()

		select {
		case conf := <-confChan:
			select {
			case b.confChan <- conf:

			case <-ctx.Done():
			}

		case err := <-errChan:
			b.writeToConfErrChan(ctx, err)

			b.writeToErrChan(fmt.Errorf("confirmations "+
				"monitoring error: %w", err))

		case <-ctx.Done():
		}
	}()

	return nil
}

// getFeePortionForSweep calculates the fee portion that each sweep should pay
// for the batch transaction. The fee is split evenly among the sweeps, If the
// fee cannot be split evenly, the remainder is paid by the first sweep.
func getFeePortionForSweep(spendTx *wire.MsgTx, numSweeps int,
	totalSweptAmt btcutil.Amount) (btcutil.Amount, btcutil.Amount) {

	totalFee := int64(totalSweptAmt)
	for _, txOut := range spendTx.TxOut {
		totalFee -= txOut.Value
	}
	feePortionPerSweep := totalFee / int64(numSweeps)
	roundingDiff := totalFee - (int64(numSweeps) * feePortionPerSweep)

	return btcutil.Amount(feePortionPerSweep), btcutil.Amount(roundingDiff)
}

// getFeePortionPaidBySweep returns the fee portion that the sweep should pay
// for the batch transaction. If the sweep is the primary sweep in the batch, it
// pays the rounding difference.
func getFeePortionPaidBySweep(feePortionPerSweep, roundingDiff btcutil.Amount,
	primary bool) btcutil.Amount {

	if primary {
		return feePortionPerSweep + roundingDiff
	}

	return feePortionPerSweep
}

// handleSpend handles a spend notification.
func (b *batch) handleSpend(ctx context.Context, spendTx *wire.MsgTx) error {
	var (
		txHash     = spendTx.TxHash()
		notifyList = make([]sweep, 0, len(b.sweeps))
	)
	b.batchTxid = &txHash
	if len(spendTx.TxOut) > 0 {
		b.batchPkScript = spendTx.TxOut[0].PkScript
	} else {
		b.Warnf("transaction %v has no outputs", txHash)
	}

	// Make a set of confirmed sweeps.
	confirmedSet := make(map[wire.OutPoint]struct{}, len(spendTx.TxIn))
	for _, txIn := range spendTx.TxIn {
		confirmedSet[txIn.PreviousOutPoint] = struct{}{}
	}

	// As a previous version of the batch transaction may get confirmed,
	// which does not contain the latest sweeps, we need to detect which
	// sweeps are in the transaction to correctly calculate fee portions
	// and notify proper sweeps.
	var (
		totalSweptAmt   btcutil.Amount
		confirmedSweeps = []wire.OutPoint{}
	)
	for _, sweep := range b.sweeps {
		// Skip sweeps that were not included into the confirmed tx.
		_, found := confirmedSet[sweep.outpoint]
		if !found {
			continue
		}

		totalSweptAmt += sweep.value
		notifyList = append(notifyList, sweep)
		confirmedSweeps = append(confirmedSweeps, sweep.outpoint)
	}

	// Calculate the fee portion that each sweep should pay for the batch.
	feePortionPaidPerSweep, roundingDifference := getFeePortionForSweep(
		spendTx, len(notifyList), totalSweptAmt,
	)

	// Calculate fees per swaps. Only the first sweep in a swap has a
	// notifier, so we calculate total fee per swap and send it to a sweep
	// having that swap and a notifier.
	swap2fee := make(map[lntypes.Hash]btcutil.Amount)
	for _, sweep := range notifyList {
		primary := sweep.outpoint == b.primarySweepID

		swap2fee[sweep.swapHash] += getFeePortionPaidBySweep(
			feePortionPaidPerSweep, roundingDifference, primary,
		)
	}

	// Now send notifications to notifiers.
	for _, sweep := range notifyList {
		// If the sweep's notifier is empty then this means that a swap
		// is not waiting to read an update from it or this is not the
		// first sweep in a swap, so we can skip the notification part.
		if sweep.notifier == nil ||
			*sweep.notifier == (SpendNotifier{}) {

			continue
		}

		// Make sure there is only one sweep with a notifier per swap
		// hash, otherwise our fee calculation is incorrect.
		fee, has := swap2fee[sweep.swapHash]
		if !has {
			return fmt.Errorf("no fee for swap %v; maybe "+
				"multiple sweeps with a notifier per swap?",
				sweep.swapHash)
		}
		delete(swap2fee, sweep.swapHash)

		spendDetail := SpendDetail{
			Tx:                spendTx,
			OnChainFeePortion: fee,
		}

		// Dispatch the sweep notifier, we don't care about the outcome
		// of this action so we don't wait for it.
		go func() {
			// Make sure this context doesn't expire so we
			// successfully notify the caller.
			ctx := context.WithoutCancel(ctx)

			sweep.notifySweepSpend(ctx, &spendDetail)
		}()
	}

	b.Infof("spent, confirmed sweeps: %v", confirmedSweeps)

	// We are no longer able to accept new sweeps, so we mark the batch as
	// closed and persist on storage.
	b.state = Closed

	if err := b.persist(ctx); err != nil {
		return fmt.Errorf("saving batch failed: %w", err)
	}

	if err := b.monitorConfirmations(ctx); err != nil {
		return fmt.Errorf("monitorConfirmations failed: %w", err)
	}

	return nil
}

// handleConf handles a confirmation notification. This is the final step of the
// batch. Here we signal to the batcher that this batch was completed.
func (b *batch) handleConf(ctx context.Context,
	conf *chainntnfs.TxConfirmation) error {

	spendTx := conf.Tx
	if spendTx == nil {
		return fmt.Errorf("confirmation doesn't have spendTx, "+
			"height=%d, TxIndex=%d", conf.BlockHeight, conf.TxIndex)
	}
	txHash := spendTx.TxHash()
	if b.batchTxid == nil || *b.batchTxid != txHash {
		b.Warnf("Mismatch of batch txid: tx in spend notification had "+
			"txid %v, but confirmation notification has txif %v. "+
			"Using the later.", b.batchTxid, txHash)
	}
	b.batchTxid = &txHash

	b.Infof("confirmed in txid %s", b.batchTxid)
	b.state = Confirmed

	// If the batch is in presigned mode, cleanup presignedHelper.
	presigned, err := b.isPresigned()
	if err != nil {
		return fmt.Errorf("failed to determine if the batch %d uses "+
			"presigned mode: %w", b.id, err)
	}

	// Sort sweeps by the addition order. This is important in presigned
	// mode to pass them in correct order to purger (AddSweep) so the
	// primary sweep is determined correctly and the presigned transaction
	// is found. In regular mode the order doesn't matter, but we do it the
	// same way for simplicity.
	allSweeps, err := b.getOrderedSweeps(ctx)
	if err != nil {
		return fmt.Errorf("getOrderedSweeps(%d) failed: %w",
			b.id, err)
	}

	// Make a set of confirmed sweeps.
	confirmedSet := make(map[wire.OutPoint]struct{}, len(spendTx.TxIn))
	for _, txIn := range spendTx.TxIn {
		confirmedSet[txIn.PreviousOutPoint] = struct{}{}
	}

	// As a previous version of the batch transaction may get confirmed,
	// which does not contain the latest sweeps, we need to detect the
	// sweeps that did not make it to the confirmed transaction and feed
	// them back to the batcher. This will ensure that the sweeps will enter
	// a new batch instead of remaining dangling.
	var (
		confirmedSweeps = []wire.OutPoint{}
		purgeList       = make([]SweepRequest, 0, len(b.sweeps))
		totalSweptAmt   btcutil.Amount
		dbConfirmed     = make([]*dbSweep, 0, len(allSweeps))
	)
	for _, sweep := range allSweeps {
		_, found := confirmedSet[sweep.outpoint]
		if found {
			// Save the sweep as completed; the batch row and all
			// sweeps are persisted atomically below.
			dbConfirmed = append(
				dbConfirmed, b.dbSweepFrom(sweep, true),
			)

			confirmedSweeps = append(
				confirmedSweeps, sweep.outpoint,
			)

			totalSweptAmt += sweep.value

			continue
		}

		// If the sweep's outpoint was not found in the transaction's
		// inputs this means it was left out. So we delete it from this
		// batch and feed it back to the batcher.
		newSweep := sweep
		delete(b.sweeps, sweep.outpoint)

		newInput := Input{
			Outpoint: newSweep.outpoint,
			Value:    newSweep.value,
		}

		// In presigned mode we should form a SweepRequest per swap
		// (i.e. per group) and keep them ordered. It should reproduce
		// the arguments and the order of the original external AddSweep
		// calls.
		L := len(purgeList)
		if presigned && L != 0 &&
			purgeList[L-1].SwapHash == newSweep.swapHash {

			// Add the input to existing SweepRequest for this swap.
			purgeList[L-1].Inputs = append(
				purgeList[L-1].Inputs, newInput,
			)
		} else {
			// Add the current sweep as a new element to purgeList.
			// This is possible either in regular mode or in
			// presigned mode in the beginning or on new swap.
			purgeList = append(purgeList, SweepRequest{
				SwapHash: newSweep.swapHash,
				Inputs:   []Input{newInput},
				Notifier: newSweep.notifier,
			})
		}
	}
	var (
		purgedSweeps = []wire.OutPoint{}
		purgedSwaps  = []lntypes.Hash{}
	)
	for _, sweepReq := range purgeList {
		purgedSwaps = append(purgedSwaps, sweepReq.SwapHash)
		for _, input := range sweepReq.Inputs {
			purgedSweeps = append(purgedSweeps, input.Outpoint)
		}
	}

	b.Infof("Fully confirmed sweeps: %v, purged sweeps: %v, "+
		"purged swaps: %v. Saving the batch and sweeps to DB",
		confirmedSweeps, purgedSweeps, purgedSwaps)

	if err := b.persistConfirmedBatch(ctx, dbConfirmed); err != nil {
		return fmt.Errorf("saving confirmed batch failed: %w", err)
	}

	b.Infof("Successfully saved the batch and confirmed sweeps to DB")

	// Proceed with purging the sweeps. This will feed the sweeps that
	// didn't make it to the confirmed batch transaction back to the batcher
	// for re-entry. This batch doesn't care for the outcome of this
	// operation so we don't wait for it.
	go func() {
		// Make sure this context doesn't expire so we successfully
		// add the sweeps to the batcher.
		ctx := context.WithoutCancel(ctx)

		// Iterate over the purge list and feed the sweeps back to the
		// batcher.
		for _, sweepReq := range purgeList {
			err := b.purger(ctx, &sweepReq)
			if err != nil {
				b.Errorf("unable to purge sweep group %x: %v",
					sweepReq.SwapHash[:6], err)
			}
		}
	}()

	if presigned {
		b.Infof("Cleaning up presigned store")

		inputs := make([]wire.OutPoint, 0, len(spendTx.TxIn))
		for _, txIn := range spendTx.TxIn {
			inputs = append(inputs, txIn.PreviousOutPoint)
		}

		err := b.cfg.presignedHelper.CleanupTransactions(ctx, inputs)
		if err != nil {
			return fmt.Errorf("failed to clean up store for "+
				"batch %d, inputs %v: %w", b.id, inputs, err)
		}
	}

	// Calculate the fee portion that each sweep should pay for the batch.
	feePortionPaidPerSweep, roundingDifference := getFeePortionForSweep(
		spendTx, len(b.sweeps), totalSweptAmt,
	)

	// Calculate fees per swaps. Only the first sweep in a swap has a
	// notifier, so we calculate total fee per swap and send it to a sweep
	// having that swap and a notifier.
	swap2fee := make(map[lntypes.Hash]btcutil.Amount)
	for _, sweep := range b.sweeps {
		primary := sweep.outpoint == b.primarySweepID

		swap2fee[sweep.swapHash] += getFeePortionPaidBySweep(
			feePortionPaidPerSweep, roundingDifference, primary,
		)
	}

	// Send the confirmation to all the notifiers.
	for _, s := range b.sweeps {
		// If the sweep's notifier is empty then this means that
		// a swap is not waiting to read an update from it, so
		// we can skip the notification part.
		if s.notifier == nil || s.notifier.ConfChan == nil {
			continue
		}

		// Make sure there is only one sweep with a notifier per swap
		// hash, otherwise our fee calculation is incorrect.
		fee, has := swap2fee[s.swapHash]
		if !has {
			return fmt.Errorf("no fee for swap %v; maybe "+
				"multiple sweeps with a notifier per swap?",
				s.swapHash)
		}
		delete(swap2fee, s.swapHash)

		confDetail := &ConfDetail{
			TxConfirmation:    conf,
			OnChainFeePortion: fee,
		}

		// Notify the caller in a goroutine to avoid possible dead-lock.
		go func(notifier *SpendNotifier) {
			// Note that we don't unblock on ctx, because it will
			// expire soon, when batch.Run completes. The caller is
			// responsible to consume ConfChan or close QuitChan.
			select {
			// Try to write the confirmation to the notification
			// channel.
			case notifier.ConfChan <- confDetail:

			// If a quit signal was provided by the swap,
			// continue.
			case <-notifier.QuitChan:
			}
		}(s.notifier)
	}

	return nil
}

// isComplete returns true if the batch is completed. This method is used by the
// batcher for lazy deletion of batches.
func (b *batch) isComplete() bool {
	done, err := b.scheduleNextCall()
	defer done()

	// We override the ErrBatchShuttingDown error as that is the expected
	// error to be returned by the scheduler once the batch's main run loop
	// has exited.
	if err != nil && err != ErrBatchShuttingDown {
		return false
	}

	return b.state == Confirmed
}

// persist updates the batch in the database.
func (b *batch) persist(ctx context.Context) error {
	return b.store.UpdateSweepBatch(ctx, b.dbBatch())
}

// dbBatch builds the dbBatch representation for the current in-memory state.
func (b *batch) dbBatch() *dbBatch {
	bch := &dbBatch{}

	bch.ID = b.id
	bch.Confirmed = b.state == Confirmed

	if b.batchTxid != nil {
		bch.BatchTxid = *b.batchTxid
	}

	bch.BatchPkScript = b.batchPkScript
	bch.LastRbfHeight = b.rbfCache.LastHeight
	bch.LastRbfSatPerKw = int32(b.rbfCache.FeeRate)
	bch.MaxTimeoutDistance = b.cfg.maxTimeoutDistance

	return bch
}

// getBatchDestAddr returns the batch's destination address. If the batch
// has already generated an address then the same one will be returned.
// The method must not be used in presigned mode. Use getPresignedSweepsDestAddr
// instead.
func (b *batch) getBatchDestAddr(ctx context.Context) (btcutil.Address, error) {
	// Determine if we should use presigned mode for the batch.
	presigned, err := b.isPresigned()
	if err != nil {
		return nil, fmt.Errorf("failed to determine if the batch %d "+
			"uses presigned mode: %w", b.id, err)
	}

	// Make sure that the method is not used for presigned batches.
	if presigned {
		return nil, fmt.Errorf("getBatchDestAddr used in presigned " +
			"mode")
	}

	var address btcutil.Address

	// If a batch address is set, use that. Otherwise, generate a
	// new address.
	if b.batchAddress != nil {
		address = b.batchAddress
	} else {
		var err error

		// Generate a wallet address for the batch transaction's output.
		address, err = b.wallet.NextAddr(
			ctx, "", walletrpc.AddressType_TAPROOT_PUBKEY, false,
		)
		if err != nil {
			return address, err
		}

		// Save that new address in order to re-use in future
		// versions of the batch tx.
		b.batchAddress = address
	}

	return address, nil
}

func (b *batch) insertAndAcquireID(ctx context.Context) (int32, error) {
	bch := &dbBatch{}
	bch.Confirmed = b.state == Confirmed
	bch.MaxTimeoutDistance = b.cfg.maxTimeoutDistance

	id, err := b.store.InsertSweepBatch(ctx, bch)
	if err != nil {
		return 0, err
	}

	b.id = id
	b.setLog(batchPrefixLogger(fmt.Sprintf("%d", b.id)))

	return id, nil
}

// notifySweepSpend writes the spendTx to the sweep's notifier channel.
func (s *sweep) notifySweepSpend(ctx context.Context,
	spendDetail *SpendDetail) {

	select {
	// Try to write the update to the notification channel.
	case s.notifier.SpendChan <- spendDetail:

	// If a quit signal was provided by the swap, continue.
	case <-s.notifier.QuitChan:

	// If the context was canceled, return.
	case <-ctx.Done():
	}
}

func (b *batch) writeToErrChan(err error) {
	select {
	case b.errChan <- err:
	default:
	}
}

// writeToSpendErrChan sends an error to spend error channels of all the sweeps.
func (b *batch) writeToSpendErrChan(ctx context.Context, spendErr error) {
	notifiers := make([]*SpendNotifier, 0, len(b.sweeps))
	for _, s := range b.sweeps {
		// If the sweep's notifier is empty then this means that a swap
		// is not waiting to read an update from it, so we can skip
		// the notification part.
		if s.notifier == nil || s.notifier.SpendErrChan == nil {
			continue
		}

		notifiers = append(notifiers, s.notifier)
	}

	for _, notifier := range notifiers {
		select {
		// Try to write the error to the notification
		// channel.
		case notifier.SpendErrChan <- spendErr:

		// If a quit signal was provided by the swap,
		// continue.
		case <-notifier.QuitChan:

		// If the context was canceled, stop.
		case <-ctx.Done():
		}
	}
}

// writeToConfErrChan sends an error to confirmation error channels of all the
// sweeps.
func (b *batch) writeToConfErrChan(ctx context.Context, confErr error) {
	done, err := b.scheduleNextCall()
	if err != nil {
		done()

		return
	}
	notifiers := make([]*SpendNotifier, 0, len(b.sweeps))
	for _, s := range b.sweeps {
		// If the sweep's notifier is empty then this means that a swap
		// is not waiting to read an update from it, so we can skip
		// the notification part.
		if s.notifier == nil || s.notifier.ConfErrChan == nil {
			continue
		}

		notifiers = append(notifiers, s.notifier)
	}
	done()

	for _, notifier := range notifiers {
		select {
		// Try to write the error to the notification
		// channel.
		case notifier.ConfErrChan <- confErr:

		// If a quit signal was provided by the swap,
		// continue.
		case <-notifier.QuitChan:

		// If the context was canceled, stop.
		case <-ctx.Done():
		}
	}
}

// persistSweep upserts the given sweep into the backing store and optionally
// marks it as completed.
func (b *batch) persistSweep(ctx context.Context, sweep sweep,
	completed bool) error {

	return b.store.UpsertSweep(ctx, b.dbSweepFrom(sweep, completed))
}

// dbSweepFrom builds the dbSweep representation for a batch sweep.
func (b *batch) dbSweepFrom(sweep sweep, completed bool) *dbSweep {
	return &dbSweep{
		BatchID:   b.id,
		SwapHash:  sweep.swapHash,
		Outpoint:  sweep.outpoint,
		Amount:    sweep.value,
		Completed: completed,
	}
}

// persistConfirmedBatch atomically records the batch confirmation metadata
// along with all sweeps that confirmed in the same transaction.
func (b *batch) persistConfirmedBatch(ctx context.Context,
	sweeps []*dbSweep) error {

	return b.store.ConfirmBatchWithSweeps(ctx, b.dbBatch(), sweeps)
}
