package loopd

import (
	"bytes"
	"cmp"
	"context"
	"encoding/hex"
	"errors"
	"fmt"
	"reflect"
	"slices"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/btcsuite/btcd/btcec/v2"
	"github.com/btcsuite/btcd/btcutil"
	"github.com/btcsuite/btcd/chaincfg"
	"github.com/btcsuite/btcd/wire"
	"github.com/lightninglabs/aperture/l402"
	"github.com/lightninglabs/lndclient"
	"github.com/lightninglabs/loop"
	"github.com/lightninglabs/loop/assets"
	"github.com/lightninglabs/loop/fsm"
	"github.com/lightninglabs/loop/instantout"
	"github.com/lightninglabs/loop/instantout/reservation"
	"github.com/lightninglabs/loop/labels"
	"github.com/lightninglabs/loop/liquidity"
	"github.com/lightninglabs/loop/loopdb"
	"github.com/lightninglabs/loop/looprpc"
	"github.com/lightninglabs/loop/staticaddr/address"
	"github.com/lightninglabs/loop/staticaddr/deposit"
	"github.com/lightninglabs/loop/staticaddr/loopin"
	"github.com/lightninglabs/loop/staticaddr/openchannel"
	"github.com/lightninglabs/loop/staticaddr/staticutil"
	"github.com/lightninglabs/loop/staticaddr/withdraw"
	"github.com/lightninglabs/loop/swap"
	"github.com/lightninglabs/loop/swapserverrpc"
	"github.com/lightninglabs/taproot-assets/rfqmath"
	"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
	"github.com/lightningnetwork/lnd/lntypes"
	"github.com/lightningnetwork/lnd/queue"
	"github.com/lightningnetwork/lnd/routing/route"
	"github.com/lightningnetwork/lnd/zpay32"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

const (
	completedSwapsCount = 5

	// minConfTarget is the minimum confirmation target we'll allow clients
	// to specify. This is driven by the minimum confirmation target allowed
	// by the backing fee estimator.
	minConfTarget = 2

	defaultLoopdInitiator = "loopd"
)

var (
	// errIncorrectChain is returned when the format of the
	// destination address provided does not match the active chain.
	errIncorrectChain = errors.New("invalid address format for the " +
		"active chain")

	// errConfTargetTooLow is returned when the chosen confirmation target
	// is below the allowed minimum.
	errConfTargetTooLow = errors.New("confirmation target too low")

	// errBalanceTooLow is returned when the loop out amount can't be
	// satisfied given total balance of the selection of channels to loop
	// out on.
	errBalanceTooLow = errors.New(
		"channel balance too low for loop out amount",
	)

	// errInvalidAddress is returned when the destination address is of
	// an unsupported format such as P2PK or P2TR addresses.
	errInvalidAddress = errors.New(
		"invalid or unsupported address",
	)
)

// swapClientServer implements the grpc service exposed by loopd.
type swapClientServer struct {
	// Required by the grpc-gateway/v2 library for forward compatibility.
	looprpc.UnimplementedSwapClientServer
	looprpc.UnimplementedDebugServer

	config               *Config
	network              lndclient.Network
	impl                 *loop.Client
	liquidityMgr         *liquidity.Manager
	lnd                  *lndclient.LndServices
	reservationManager   *reservation.Manager
	instantOutManager    *instantout.Manager
	staticAddressManager *address.Manager
	depositManager       *deposit.Manager
	withdrawalManager    *withdraw.Manager
	staticLoopInManager  *loopin.Manager
	openChannelManager   *openchannel.Manager
	assetClient          *assets.TapdClient
	swaps                map[lntypes.Hash]loop.SwapInfo
	subscribers          map[int]chan<- interface{}
	statusChan           chan loop.SwapInfo
	nextSubscriberID     int
	swapsLock            sync.Mutex
	mainCtx              context.Context

	// stopDaemon is invoked to trigger a graceful shutdown of the daemon.
	stopDaemon func()
}

// LoopOut initiates a loop out swap with the given parameters. The call returns
// after the swap has been set up with the swap server. From that point onwards,
// progress can be tracked via the LoopOutStatus stream that is returned from
// Monitor().
func (s *swapClientServer) LoopOut(ctx context.Context,
	in *looprpc.LoopOutRequest) (
	*looprpc.SwapResponse, error) {

	infof("Loop out request received")

	// Note that LoopOutRequest.PaymentTimeout is unsigned and therefore
	// cannot be negative.
	paymentTimeout := time.Duration(in.PaymentTimeout) * time.Second

	// Make sure we don't exceed the total allowed payment timeout.
	if paymentTimeout > s.config.TotalPaymentTimeout {
		return nil, fmt.Errorf("payment timeout %v exceeds maximum "+
			"allowed timeout of %v", paymentTimeout,
			s.config.TotalPaymentTimeout)
	}

	var sweepAddr btcutil.Address
	var isExternalAddr bool
	var err error
	//nolint:lll
	switch {
	case in.Dest != "" && in.Account != "":
		return nil, fmt.Errorf("destination address and external " +
			"account address cannot be set at the same time")

	case in.Dest != "":
		// Decode the client provided destination address for the loop
		// out sweep.
		sweepAddr, err = btcutil.DecodeAddress(
			in.Dest, s.lnd.ChainParams,
		)
		if err != nil {
			return nil, fmt.Errorf("decode address: %v", err)
		}

		isExternalAddr = true

	case in.Account != "" && in.AccountAddrType == looprpc.AddressType_ADDRESS_TYPE_UNKNOWN:
		return nil, liquidity.ErrAccountAndAddrType

	case in.Account != "":
		// Derive a new receiving address from the stated account.
		addrType, err := toWalletAddrType(in.AccountAddrType)
		if err != nil {
			return nil, err
		}

		// Check if account with address type exists.
		if !s.accountExists(ctx, in.Account, addrType) {
			return nil, fmt.Errorf("the provided account does " +
				"not exist")
		}

		sweepAddr, err = s.lnd.WalletKit.NextAddr(
			ctx, in.Account, addrType, false,
		)
		if err != nil {
			return nil, fmt.Errorf("NextAddr from account error: "+
				"%v", err)
		}

		isExternalAddr = true

	default:
		// Generate sweep address if none specified.
		sweepAddr, err = s.lnd.WalletKit.NextAddr(
			context.Background(), "",
			walletrpc.AddressType_WITNESS_PUBKEY_HASH, false,
		)
		if err != nil {
			return nil, fmt.Errorf("NextAddr error: %v", err)
		}
	}

	sweepConfTarget, err := validateLoopOutRequest(
		ctx, s.lnd.Client, s.lnd.ChainParams, in, sweepAddr,
		s.impl.LoopOutMaxParts,
	)
	if err != nil {
		return nil, err
	}

	// Infer if the publication deadline is set in milliseconds.
	publicationDeadline := getPublicationDeadline(in.SwapPublicationDeadline)

	req := &loop.OutRequest{
		Amount:                  btcutil.Amount(in.Amt),
		DestAddr:                sweepAddr,
		IsExternalAddr:          isExternalAddr,
		MaxMinerFee:             btcutil.Amount(in.MaxMinerFee),
		MaxPrepayAmount:         btcutil.Amount(in.MaxPrepayAmt),
		MaxPrepayRoutingFee:     btcutil.Amount(in.MaxPrepayRoutingFee),
		MaxSwapRoutingFee:       btcutil.Amount(in.MaxSwapRoutingFee),
		MaxSwapFee:              btcutil.Amount(in.MaxSwapFee),
		SweepConfTarget:         sweepConfTarget,
		HtlcConfirmations:       in.HtlcConfirmations,
		SwapPublicationDeadline: publicationDeadline,
		Label:                   in.Label,
		Initiator:               in.Initiator,
		PaymentTimeout:          paymentTimeout,
	}

	// If the asset id is set, we need to set the asset amount and asset id
	// in the request.
	if in.AssetInfo != nil {
		if len(in.AssetInfo.AssetId) != 0 &&
			len(in.AssetInfo.AssetId) != 32 {

			return nil, fmt.Errorf(
				"asset id must be set to a 32 byte value",
			)
		}

		if len(in.AssetRfqInfo.PrepayRfqId) != 0 &&
			len(in.AssetRfqInfo.PrepayRfqId) != 32 {

			return nil, fmt.Errorf(
				"prepay rfq id must be set to a 32 byte value",
			)
		}

		if len(in.AssetRfqInfo.SwapRfqId) != 0 &&
			len(in.AssetRfqInfo.SwapRfqId) != 32 {

			return nil, fmt.Errorf(
				"swap rfq id must be set to a 32 byte value",
			)
		}

		req.AssetId = in.AssetInfo.AssetId
		req.AssetPrepayRfqId = in.AssetRfqInfo.PrepayRfqId
		req.AssetSwapRfqId = in.AssetRfqInfo.SwapRfqId
	}

	switch {
	case in.LoopOutChannel != 0 && len(in.OutgoingChanSet) > 0: // nolint:staticcheck
		return nil, errors.New("loop_out_channel and outgoing_" +
			"chan_ids are mutually exclusive")

	case in.LoopOutChannel != 0: // nolint:staticcheck
		req.OutgoingChanSet = loopdb.ChannelSet{in.LoopOutChannel} // nolint:staticcheck

	default:
		req.OutgoingChanSet = in.OutgoingChanSet
	}

	info, err := s.impl.LoopOut(ctx, req)
	if err != nil {
		errorf("LoopOut: %v", err)
		return nil, err
	}

	htlcAddress := info.HtlcAddress.String()
	resp := &looprpc.SwapResponse{
		Id:            info.SwapHash.String(),
		IdBytes:       info.SwapHash[:],
		HtlcAddress:   htlcAddress,
		ServerMessage: info.ServerMessage,
	}

	if loopdb.CurrentProtocolVersion() < loopdb.ProtocolVersionHtlcV3 {
		resp.HtlcAddressP2Wsh = htlcAddress
	} else {
		resp.HtlcAddressP2Tr = htlcAddress
	}

	return resp, nil
}

// accountExists returns true if account under the address type exists in the
// backing lnd instance and false otherwise.
func (s *swapClientServer) accountExists(ctx context.Context, account string,
	addrType walletrpc.AddressType) bool {

	accounts, err := s.lnd.WalletKit.ListAccounts(ctx, account, addrType)
	if err != nil {
		return false
	}

	for _, a := range accounts {
		if a.Name == account {
			return true
		}
	}

	return false
}

func toWalletAddrType(addrType looprpc.AddressType) (walletrpc.AddressType,
	error) {

	switch addrType {
	case looprpc.AddressType_TAPROOT_PUBKEY:
		return walletrpc.AddressType_TAPROOT_PUBKEY, nil

	default:
		return walletrpc.AddressType_UNKNOWN,
			fmt.Errorf("unknown address type")
	}
}

func (s *swapClientServer) marshallSwap(ctx context.Context,
	loopSwap *loop.SwapInfo) (*looprpc.SwapStatus, error) {

	var (
		state         looprpc.SwapState
		failureReason = looprpc.FailureReason_FAILURE_REASON_NONE
	)

	// Set our state var for non-failure states. If we get a failure, we
	// will update our failure reason. To remain backwards compatible with
	// previous versions where we squashed all failure reasons to a single
	// failure state, we set a failure reason for all our different failure
	// states, and set our failed state for all of them.
	switch loopSwap.State {
	case loopdb.StateInitiated:
		state = looprpc.SwapState_INITIATED

	case loopdb.StatePreimageRevealed:
		state = looprpc.SwapState_PREIMAGE_REVEALED

	case loopdb.StateHtlcPublished:
		state = looprpc.SwapState_HTLC_PUBLISHED

	case loopdb.StateInvoiceSettled:
		state = looprpc.SwapState_INVOICE_SETTLED

	case loopdb.StateSuccess:
		state = looprpc.SwapState_SUCCESS

	case loopdb.StateFailOffchainPayments:
		failureReason = looprpc.FailureReason_FAILURE_REASON_OFFCHAIN

	case loopdb.StateFailTimeout:
		failureReason = looprpc.FailureReason_FAILURE_REASON_TIMEOUT

	case loopdb.StateFailSweepTimeout:
		failureReason = looprpc.FailureReason_FAILURE_REASON_SWEEP_TIMEOUT

	case loopdb.StateFailInsufficientValue:
		failureReason = looprpc.FailureReason_FAILURE_REASON_INSUFFICIENT_VALUE

	case loopdb.StateFailTemporary:
		failureReason = looprpc.FailureReason_FAILURE_REASON_TEMPORARY

	case loopdb.StateFailIncorrectHtlcAmt:
		failureReason = looprpc.FailureReason_FAILURE_REASON_INCORRECT_AMOUNT

	case loopdb.StateFailAbandoned:
		failureReason = looprpc.FailureReason_FAILURE_REASON_ABANDONED

	case loopdb.StateFailInsufficientConfirmedBalance:
		failureReason = looprpc.FailureReason_FAILURE_REASON_INSUFFICIENT_CONFIRMED_BALANCE

	case loopdb.StateFailIncorrectHtlcAmtSwept:
		failureReason = looprpc.FailureReason_FAILURE_REASON_INCORRECT_HTLC_AMT_SWEPT

	default:
		return nil, fmt.Errorf("unknown swap state: %v", loopSwap.State)
	}

	// If we have a failure reason, we have a failure state, so should use
	// our catchall failed state.
	if failureReason != looprpc.FailureReason_FAILURE_REASON_NONE {
		state = looprpc.SwapState_FAILED
	}

	var swapType looprpc.SwapType
	var (
		htlcAddress      string
		htlcAddressP2TR  string
		htlcAddressP2WSH string
	)
	var outGoingChanSet []uint64
	var lastHop []byte
	var assetInfo *looprpc.AssetLoopOutInfo

	switch loopSwap.SwapType {
	case swap.TypeIn:
		swapType = looprpc.SwapType_LOOP_IN

		if loopSwap.HtlcAddressP2TR != nil {
			htlcAddressP2TR = loopSwap.HtlcAddressP2TR.EncodeAddress()
			htlcAddress = htlcAddressP2TR
		} else {
			htlcAddressP2WSH =
				loopSwap.HtlcAddressP2WSH.EncodeAddress()
			htlcAddress = htlcAddressP2WSH
		}

		if loopSwap.LastHop != nil {
			lastHop = loopSwap.LastHop[:]
		}

	case swap.TypeOut:
		swapType = looprpc.SwapType_LOOP_OUT
		if loopSwap.HtlcAddressP2WSH != nil {
			htlcAddressP2WSH = loopSwap.HtlcAddressP2WSH.EncodeAddress()
			htlcAddress = htlcAddressP2WSH
		} else {
			htlcAddressP2TR = loopSwap.HtlcAddressP2TR.EncodeAddress()
			htlcAddress = htlcAddressP2TR
		}

		outGoingChanSet = loopSwap.OutgoingChanSet

		if loopSwap.AssetSwapInfo != nil {
			var (
				// Default the asset name to "N/A" in case we
				// can't fetch it due to the asset client not
				// being set.
				assetName string = "N/A"
				err       error
			)
			if s.assetClient != nil {
				assetName, err = s.assetClient.GetAssetName(
					ctx, loopSwap.AssetSwapInfo.AssetId,
				)
				if err != nil {
					return nil, err
				}
			}

			assetInfo = &looprpc.AssetLoopOutInfo{
				AssetId: hex.EncodeToString(loopSwap.AssetSwapInfo.AssetId), // nolint:lll
				AssetCostOffchain: loopSwap.AssetSwapInfo.PrepayPaidAmt +
					loopSwap.AssetSwapInfo.SwapPaidAmt, // nolint:lll
				AssetName: assetName,
			}
		}

	default:
		return nil, errors.New("unknown swap type")
	}

	return &looprpc.SwapStatus{
		Amt:              int64(loopSwap.AmountRequested),
		Id:               loopSwap.SwapHash.String(),
		IdBytes:          loopSwap.SwapHash[:],
		State:            state,
		FailureReason:    failureReason,
		InitiationTime:   loopSwap.InitiationTime.UnixNano(),
		LastUpdateTime:   loopSwap.LastUpdate.UnixNano(),
		HtlcAddress:      htlcAddress,
		HtlcAddressP2Tr:  htlcAddressP2TR,
		HtlcAddressP2Wsh: htlcAddressP2WSH,
		Type:             swapType,
		CostServer:       int64(loopSwap.Cost.Server),
		CostOnchain:      int64(loopSwap.Cost.Onchain),
		CostOffchain:     int64(loopSwap.Cost.Offchain),
		Label:            loopSwap.Label,
		LastHop:          lastHop,
		OutgoingChanSet:  outGoingChanSet,
		AssetInfo:        assetInfo,
	}, nil
}

// Monitor will return a stream of swap updates for currently active swaps.
func (s *swapClientServer) Monitor(in *looprpc.MonitorRequest,
	server looprpc.SwapClient_MonitorServer) error {

	infof("Monitor request received")

	send := func(info loop.SwapInfo) error {
		rpcSwap, err := s.marshallSwap(server.Context(), &info)
		if err != nil {
			return err
		}

		return server.Send(rpcSwap)
	}

	// Start a notification queue for this subscriber.
	queue := queue.NewConcurrentQueue(20)
	queue.Start()

	// Add this subscriber to the global subscriber list. Also create a
	// snapshot of all pending and completed swaps within the lock, to
	// prevent subscribers from receiving duplicate updates.
	s.swapsLock.Lock()

	id := s.nextSubscriberID
	s.nextSubscriberID++
	s.subscribers[id] = queue.ChanIn()

	var pendingSwaps, completedSwaps []loop.SwapInfo
	for _, swap := range s.swaps {
		if swap.State.Type() == loopdb.StateTypePending {
			pendingSwaps = append(pendingSwaps, swap)
		} else {
			completedSwaps = append(completedSwaps, swap)
		}
	}

	s.swapsLock.Unlock()

	defer func() {
		s.swapsLock.Lock()
		delete(s.subscribers, id)
		s.swapsLock.Unlock()
		queue.Stop()
	}()

	// Sort completed swaps new to old.
	sort.Slice(completedSwaps, func(i, j int) bool {
		return completedSwaps[i].LastUpdate.After(
			completedSwaps[j].LastUpdate,
		)
	})

	// Discard all but top x latest.
	if len(completedSwaps) > completedSwapsCount {
		completedSwaps = completedSwaps[:completedSwapsCount]
	}

	// Concatenate both sets.
	filteredSwaps := append(pendingSwaps, completedSwaps...) // nolint: gocritic

	// Sort again, but this time old to new.
	sort.Slice(filteredSwaps, func(i, j int) bool {
		return filteredSwaps[i].LastUpdate.Before(
			filteredSwaps[j].LastUpdate,
		)
	})

	// Return swaps to caller.
	for _, swap := range filteredSwaps {
		if err := send(swap); err != nil {
			return err
		}
	}

	// As long as the client is connected, keep passing through swap
	// updates.
	for {
		select {
		case queueItem, ok := <-queue.ChanOut():
			if !ok {
				return nil
			}

			swap := queueItem.(loop.SwapInfo)
			if err := send(swap); err != nil {
				return err
			}

		// The client cancels the subscription.
		case <-server.Context().Done():
			return nil

		// The server is shutting down.
		case <-s.mainCtx.Done():
			return fmt.Errorf("server is shutting down")
		}
	}
}

// ListSwaps returns a list of all currently known swaps and their current
// status.
func (s *swapClientServer) ListSwaps(ctx context.Context,
	req *looprpc.ListSwapsRequest) (*looprpc.ListSwapsResponse, error) {

	var (
		rpcSwaps      = []*looprpc.SwapStatus{}
		swapInfos     = []*loop.SwapInfo{}
		maxSwaps      = int(req.MaxSwaps)
		nextStartTime = int64(0)
		canPage       = false
	)

	s.swapsLock.Lock()
	defer s.swapsLock.Unlock()

	// We can just use the server's in-memory cache as that contains the
	// most up-to-date state including temporary failures which aren't
	// persisted to disk. The swaps field is a map, that's why we need an
	// additional index.
	for _, swp := range s.swaps {
		// Filter the swap based on the provided filter.
		if !filterSwap(&swp, req.ListSwapFilter) {
			continue
		}

		swapInfos = append(swapInfos, &swp)
	}

	// Sort the swaps by initiation time in ascending order (oldest first).
	slices.SortFunc(swapInfos, func(a, b *loop.SwapInfo) int {
		return cmp.Compare(
			a.InitiationTime.UnixNano(),
			b.InitiationTime.UnixNano(),
		)
	})

	// Apply the maxSwaps limit if specified.
	if maxSwaps > 0 && len(swapInfos) > maxSwaps {
		canPage = true
		swapInfos = swapInfos[:maxSwaps]
	}

	// Marshal the filtered and limited swaps.
	for _, swp := range swapInfos {
		rpcSwap, err := s.marshallSwap(ctx, swp)
		if err != nil {
			return nil, err
		}
		rpcSwaps = append(rpcSwaps, rpcSwap)
	}

	// Set the next start time for pagination if needed.
	if canPage && len(rpcSwaps) > 0 {
		// Use the initiation time of the last swap plus 1 nanosecond.
		nextStartTime = rpcSwaps[len(rpcSwaps)-1].InitiationTime + 1
	}

	response := looprpc.ListSwapsResponse{
		Swaps:         rpcSwaps,
		NextStartTime: nextStartTime,
	}
	return &response, nil
}

// filterSwap filters the given swap based on the provided filter.
func filterSwap(swapInfo *loop.SwapInfo, filter *looprpc.ListSwapsFilter) bool {
	if filter == nil {
		return true
	}

	// If the swap type filter is set, we only return swaps that match the
	// filter.
	if filter.SwapType != looprpc.ListSwapsFilter_ANY {
		switch filter.SwapType {
		case looprpc.ListSwapsFilter_LOOP_IN:
			if swapInfo.SwapType != swap.TypeIn {
				return false
			}

		case looprpc.ListSwapsFilter_LOOP_OUT:
			if swapInfo.SwapType != swap.TypeOut {
				return false
			}
		}
	}

	// If the pending only filter is set, we only return pending swaps.
	if filter.PendingOnly && !swapInfo.State.IsPending() {
		return false
	}

	// If timestamp filters are set, only return swaps within the specified time range.
	if filter.StartTimestampNs > 0 &&
		swapInfo.InitiationTime.UnixNano() < filter.StartTimestampNs {

		return false
	}

	// If the swap is of type loop out and the outgoing channel filter is
	// set, we only return swaps that match the filter.
	if swapInfo.SwapType == swap.TypeOut && filter.OutgoingChanSet != nil {
		// First we sort both channel sets to make sure we can compare
		// them.
		sort.Slice(swapInfo.OutgoingChanSet, func(i, j int) bool {
			return swapInfo.OutgoingChanSet[i] <
				swapInfo.OutgoingChanSet[j]
		})
		sort.Slice(filter.OutgoingChanSet, func(i, j int) bool {
			return filter.OutgoingChanSet[i] <
				filter.OutgoingChanSet[j]
		})

		// Compare the outgoing channel set by using reflect.DeepEqual
		// which compares the underlying arrays.
		if !reflect.DeepEqual(swapInfo.OutgoingChanSet,
			filter.OutgoingChanSet) {

			return false
		}
	}

	// If the swap is of type loop in and the last hop filter is set, we
	// only return swaps that match the filter.
	if swapInfo.SwapType == swap.TypeIn && filter.LoopInLastHop != nil {
		// Compare the last hop by using reflect.DeepEqual which
		// compares the underlying arrays.
		if !reflect.DeepEqual(swapInfo.LastHop, filter.LoopInLastHop) {
			return false
		}
	}

	// If a label filter is set, we only return swaps that softly match the
	// filter.
	if filter.Label != "" {
		if !strings.Contains(swapInfo.Label, filter.Label) {
			return false
		}
	}

	// If we only want to return asset swaps, we only return swaps that have
	// an asset id set.
	if filter.AssetSwapOnly && swapInfo.AssetSwapInfo == nil {
		return false
	}

	return true
}

// SwapInfo returns all known details about a single swap.
func (s *swapClientServer) SwapInfo(ctx context.Context,
	req *looprpc.SwapInfoRequest) (*looprpc.SwapStatus, error) {

	swapHash, err := lntypes.MakeHash(req.Id)
	if err != nil {
		return nil, fmt.Errorf("error parsing swap hash: %v", err)
	}

	// Just return the server's in-memory cache here too as we also want to
	// return temporary failures to the client.
	swp, ok := s.swaps[swapHash]
	if !ok {
		return nil, fmt.Errorf("swap with hash %s not found", req.Id)
	}
	return s.marshallSwap(ctx, &swp)
}

// AbandonSwap requests the server to abandon a swap with the given hash.
func (s *swapClientServer) AbandonSwap(ctx context.Context,
	req *looprpc.AbandonSwapRequest) (*looprpc.AbandonSwapResponse,
	error) {

	if !req.IKnowWhatIAmDoing {
		return nil, fmt.Errorf("please read the AbandonSwap API " +
			"documentation")
	}

	swapHash, err := lntypes.MakeHash(req.Id)
	if err != nil {
		return nil, fmt.Errorf("error parsing swap hash: %v", err)
	}

	s.swapsLock.Lock()
	swap, ok := s.swaps[swapHash]
	s.swapsLock.Unlock()
	if !ok {
		return nil, fmt.Errorf("swap with hash %s not found", req.Id)
	}

	if swap.SwapType.IsOut() {
		return nil, fmt.Errorf("abandoning loop out swaps is not " +
			"supported yet")
	}

	// If the swap is in a final state, we cannot abandon it.
	if swap.State.IsFinal() {
		return nil, fmt.Errorf("cannot abandon swap in final state, "+
			"state = %s, hash = %s", swap.State.String(), swapHash)
	}

	err = s.impl.AbandonSwap(ctx, &loop.AbandonSwapRequest{
		SwapHash: swapHash,
	})
	if err != nil {
		return nil, fmt.Errorf("error abandoning swap: %v", err)
	}

	return &looprpc.AbandonSwapResponse{}, nil
}

// LoopOutTerms returns the terms that the server enforces for loop out swaps.
func (s *swapClientServer) LoopOutTerms(ctx context.Context,
	_ *looprpc.TermsRequest) (*looprpc.OutTermsResponse, error) {

	infof("Loop out terms request received")

	terms, err := s.impl.LoopOutTerms(ctx, defaultLoopdInitiator)
	if err != nil {
		errorf("Terms request: %v", err)
		return nil, err
	}

	return &looprpc.OutTermsResponse{
		MinSwapAmount: int64(terms.MinSwapAmount),
		MaxSwapAmount: int64(terms.MaxSwapAmount),
		MinCltvDelta:  terms.MinCltvDelta,
		MaxCltvDelta:  terms.MaxCltvDelta,
	}, nil
}

// LoopOutQuote returns a quote for a loop out swap with the provided
// parameters.
func (s *swapClientServer) LoopOutQuote(ctx context.Context,
	req *looprpc.QuoteRequest) (*looprpc.OutQuoteResponse, error) {

	confTarget, err := validateConfTarget(
		req.ConfTarget, loop.DefaultSweepConfTarget,
	)
	if err != nil {
		return nil, err
	}

	publicactionDeadline := getPublicationDeadline(
		req.SwapPublicationDeadline,
	)

	loopOutQuoteReq := &loop.LoopOutQuoteRequest{
		Amount:                  btcutil.Amount(req.Amt),
		SweepConfTarget:         confTarget,
		SwapPublicationDeadline: publicactionDeadline,
		Initiator:               defaultLoopdInitiator,
	}

	if req.AssetInfo != nil {
		if req.AssetInfo.AssetId == nil ||
			req.AssetInfo.AssetEdgeNode == nil {

			return nil, fmt.Errorf(
				"asset id and edge node must both be set")
		}
		loopOutQuoteReq.AssetRFQRequest = &loop.AssetRFQRequest{
			AssetId:            req.AssetInfo.AssetId,
			AssetEdgeNode:      req.AssetInfo.AssetEdgeNode,
			Expiry:             req.AssetInfo.Expiry,
			MaxLimitMultiplier: req.AssetInfo.MaxLimitMultiplier,
		}
	}

	quote, err := s.impl.LoopOutQuote(ctx, loopOutQuoteReq)
	if err != nil {
		return nil, err
	}

	response := &looprpc.OutQuoteResponse{
		HtlcSweepFeeSat: int64(quote.MinerFee),
		PrepayAmtSat:    int64(quote.PrepayAmount),
		SwapFeeSat:      int64(quote.SwapFee),
		SwapPaymentDest: quote.SwapPaymentDest[:],
		ConfTarget:      confTarget,
	}

	if quote.LoopOutRfq != nil {
		response.AssetRfqInfo = &looprpc.AssetRfqInfo{
			PrepayRfqId:       quote.LoopOutRfq.PrepayRfqId,
			MaxPrepayAssetAmt: quote.LoopOutRfq.MaxPrepayAssetAmt,
			PrepayAssetRate: marshalFixedPoint(
				quote.LoopOutRfq.PrepayAssetRate,
			),
			SwapRfqId:       quote.LoopOutRfq.SwapRfqId,
			MaxSwapAssetAmt: quote.LoopOutRfq.MaxSwapAssetAmt,
			SwapAssetRate: marshalFixedPoint(
				quote.LoopOutRfq.SwapAssetRate,
			),
			AssetName: quote.LoopOutRfq.AssetName,
		}
	}

	return response, nil
}

// GetLoopInTerms returns the terms that the server enforces for swaps.
func (s *swapClientServer) GetLoopInTerms(ctx context.Context,
	_ *looprpc.TermsRequest) (*looprpc.InTermsResponse, error) {

	infof("Loop in terms request received")

	terms, err := s.impl.LoopInTerms(ctx, defaultLoopdInitiator)
	if err != nil {
		errorf("Terms request: %v", err)
		return nil, err
	}

	return &looprpc.InTermsResponse{
		MinSwapAmount: int64(terms.MinSwapAmount),
		MaxSwapAmount: int64(terms.MaxSwapAmount),
	}, nil
}

// GetLoopInQuote returns a quote for a swap with the provided parameters.
func (s *swapClientServer) GetLoopInQuote(ctx context.Context,
	req *looprpc.QuoteRequest) (*looprpc.InQuoteResponse, error) {

	infof("Loop in quote request received")

	var (
		selectedAmount     = btcutil.Amount(req.Amt)
		totalDepositAmount btcutil.Amount
		autoSelectDeposits = req.AutoSelectDeposits
		err                error
	)

	htlcConfTarget, err := validateLoopInRequest(
		req.ConfTarget, req.ExternalHtlc,
		uint32(len(req.DepositOutpoints)), selectedAmount,
		autoSelectDeposits,
	)
	if err != nil {
		return nil, err
	}

	// The fast flag is only available for static loop in quote requests.
	if req.Fast {
		if !autoSelectDeposits && len(req.DepositOutpoints) == 0 {
			return nil, fmt.Errorf("fast flag is only " +
				"available for static address requests")
		}
	}

	// If deposits should be automatically selected, we do so and count the
	// number of deposits to quote for.
	numDeposits := 0
	if autoSelectDeposits {
		deposits, err := s.depositManager.GetActiveDepositsInState(
			deposit.Deposited,
		)
		if err != nil {
			return nil, fmt.Errorf("unable to retrieve all "+
				"deposits: %w", err)
		}

		// TODO(hieblmi): add params to deposit for multi-address
		//      support.
		params, err := s.staticAddressManager.GetStaticAddressParameters(
			ctx,
		)
		if err != nil {
			return nil, fmt.Errorf("unable to retrieve static "+
				"address parameters: %w", err)
		}

		info, err := s.lnd.Client.GetInfo(ctx)
		if err != nil {
			return nil, fmt.Errorf("unable to get lnd info: %w",
				err)
		}
		selectedDeposits, err := loopin.SelectDeposits(
			selectedAmount, deposits, params.Expiry,
			info.BlockHeight,
		)
		if err != nil {
			return nil, fmt.Errorf("unable to select deposits: %w",
				err)
		}

		numDeposits = len(selectedDeposits)
	} else if len(req.DepositOutpoints) > 0 {
		// If deposits are selected, we need to retrieve them to
		// calculate the total value which we request a quote for.
		depositList, err := s.ListStaticAddressDeposits(
			ctx, &looprpc.ListStaticAddressDepositsRequest{
				Outpoints: req.DepositOutpoints,
			},
		)
		if err != nil {
			return nil, err
		}

		if depositList == nil {
			return nil, fmt.Errorf("no summary returned for " +
				"deposit outpoints")
		}

		if len(req.DepositOutpoints) !=
			len(depositList.FilteredDeposits) {

			return nil, fmt.Errorf("expected %d deposits, got %d",
				len(req.DepositOutpoints),
				len(depositList.FilteredDeposits))
		} else {
			numDeposits = len(depositList.FilteredDeposits)
		}

		// In case we quote for deposits, we send the server both the
		// selected value and the number of deposits. This is so the
		// server can probe the selected value and calculate the per
		// input fee.
		for _, deposit := range depositList.FilteredDeposits {
			totalDepositAmount += btcutil.Amount(
				deposit.Value,
			)
		}

		// If a fractional amount is also selected, we check if it
		// leads to a dust change output.
		selectedAmount, err = loopin.DeduceSwapAmount(
			totalDepositAmount, selectedAmount,
		)
		if err != nil {
			return nil, fmt.Errorf("error calculating "+
				"swap amount from selected amount: %v",
				err)
		}
	}

	var (
		routeHints [][]zpay32.HopHint
		lastHop    *route.Vertex
	)

	if req.LoopInLastHop != nil {
		lastHopVertex, err := route.NewVertexFromBytes(
			req.LoopInLastHop,
		)
		if err != nil {
			return nil, err
		}
		lastHop = &lastHopVertex
	}

	if len(req.LoopInRouteHints) != 0 {
		routeHints, err = unmarshallRouteHints(req.LoopInRouteHints)
		if err != nil {
			return nil, err
		}
	}

	quote, err := s.impl.LoopInQuote(ctx, &loop.LoopInQuoteRequest{
		Amount:         selectedAmount,
		HtlcConfTarget: htlcConfTarget,
		ExternalHtlc:   req.ExternalHtlc,
		LastHop:        lastHop,
		RouteHints:     routeHints,
		Private:        req.Private,
		Initiator:      defaultLoopdInitiator,
		NumDeposits:    uint32(numDeposits),
		Fast:           req.Fast,
	})
	if err != nil {
		return nil, err
	}

	return &looprpc.InQuoteResponse{
		HtlcPublishFeeSat: int64(quote.MinerFee),
		SwapFeeSat:        int64(quote.SwapFee),
		ConfTarget:        htlcConfTarget,
		QuotedAmt:         int64(selectedAmount),
	}, nil
}

// unmarshallRouteHints unmarshalls a list of route hints.
func unmarshallRouteHints(rpcRouteHints []*swapserverrpc.RouteHint) (
	[][]zpay32.HopHint, error) {

	routeHints := make([][]zpay32.HopHint, 0, len(rpcRouteHints))
	for _, rpcRouteHint := range rpcRouteHints {
		routeHint := make(
			[]zpay32.HopHint, 0, len(rpcRouteHint.HopHints),
		)
		for _, rpcHint := range rpcRouteHint.HopHints {
			hint, err := unmarshallHopHint(rpcHint)
			if err != nil {
				return nil, err
			}

			routeHint = append(routeHint, hint)
		}
		routeHints = append(routeHints, routeHint)
	}

	return routeHints, nil
}

// unmarshallHopHint unmarshalls a single hop hint.
func unmarshallHopHint(rpcHint *swapserverrpc.HopHint) (zpay32.HopHint, error) {
	pubBytes, err := hex.DecodeString(rpcHint.NodeId)
	if err != nil {
		return zpay32.HopHint{}, err
	}

	pubkey, err := btcec.ParsePubKey(pubBytes)
	if err != nil {
		return zpay32.HopHint{}, err
	}

	return zpay32.HopHint{
		NodeID:                    pubkey,
		ChannelID:                 rpcHint.ChanId,
		FeeBaseMSat:               rpcHint.FeeBaseMsat,
		FeeProportionalMillionths: rpcHint.FeeProportionalMillionths,
		CLTVExpiryDelta:           uint16(rpcHint.CltvExpiryDelta),
	}, nil
}

// Probe requests the server to probe the client's node to test inbound
// liquidity.
func (s *swapClientServer) Probe(ctx context.Context,
	req *looprpc.ProbeRequest) (*looprpc.ProbeResponse, error) {

	infof("Probe request received")

	var lastHop *route.Vertex
	if req.LastHop != nil {
		lastHopVertex, err := route.NewVertexFromBytes(req.LastHop)
		if err != nil {
			return nil, err
		}

		lastHop = &lastHopVertex
	}

	routeHints, err := unmarshallRouteHints(req.RouteHints)
	if err != nil {
		return nil, err
	}

	err = s.impl.Probe(ctx, &loop.ProbeRequest{
		Amount:     btcutil.Amount(req.Amt),
		LastHop:    lastHop,
		RouteHints: routeHints,
	})
	if err != nil {
		return nil, err
	}

	return &looprpc.ProbeResponse{}, nil
}

func (s *swapClientServer) LoopIn(ctx context.Context,
	in *looprpc.LoopInRequest) (*looprpc.SwapResponse, error) {

	infof("Loop in request received")

	selectDeposits := false
	numDeposits := uint32(0)
	htlcConfTarget, err := validateLoopInRequest(
		in.HtlcConfTarget, in.ExternalHtlc, numDeposits,
		btcutil.Amount(in.Amt), selectDeposits,
	)
	if err != nil {
		return nil, err
	}

	// Check that the label is valid.
	if err := labels.Validate(in.Label); err != nil {
		return nil, err
	}

	routeHints, err := unmarshallRouteHints(in.RouteHints)
	if err != nil {
		return nil, err
	}

	req := &loop.LoopInRequest{
		Amount:         btcutil.Amount(in.Amt),
		MaxMinerFee:    btcutil.Amount(in.MaxMinerFee),
		MaxSwapFee:     btcutil.Amount(in.MaxSwapFee),
		HtlcConfTarget: htlcConfTarget,
		ExternalHtlc:   in.ExternalHtlc,
		Label:          in.Label,
		Initiator:      in.Initiator,
		Private:        in.Private,
		RouteHints:     routeHints,
	}
	if in.LastHop != nil {
		lastHop, err := route.NewVertexFromBytes(in.LastHop)
		if err != nil {
			return nil, err
		}
		req.LastHop = &lastHop
	}
	swapInfo, err := s.impl.LoopIn(ctx, req)
	if err != nil {
		errorf("Loop in: %v", err)
		return nil, err
	}

	response := &looprpc.SwapResponse{
		Id:            swapInfo.SwapHash.String(),
		IdBytes:       swapInfo.SwapHash[:],
		ServerMessage: swapInfo.ServerMessage,
	}

	if loopdb.CurrentProtocolVersion() < loopdb.ProtocolVersionHtlcV3 {
		p2wshAddr := swapInfo.HtlcAddressP2WSH.String()
		response.HtlcAddress = p2wshAddr
		response.HtlcAddressP2Wsh = p2wshAddr
	} else {
		p2trAddr := swapInfo.HtlcAddressP2TR.String()
		response.HtlcAddress = p2trAddr
		response.HtlcAddressP2Tr = p2trAddr
	}

	return response, nil
}

// GetL402Tokens returns all tokens that are contained in the L402 token store.
func (s *swapClientServer) GetL402Tokens(ctx context.Context,
	_ *looprpc.TokensRequest) (*looprpc.TokensResponse, error) {

	infof("Get L402 tokens request received")

	tokens, err := s.impl.L402Store.AllTokens()
	if err != nil {
		return nil, err
	}

	rpcTokens := make([]*looprpc.L402Token, len(tokens))
	idx := 0
	for key, token := range tokens {
		macBytes, err := token.BaseMacaroon().MarshalBinary()
		if err != nil {
			return nil, err
		}

		id, err := l402.DecodeIdentifier(
			bytes.NewReader(token.BaseMacaroon().Id()),
		)
		if err != nil {
			return nil, err
		}
		rpcTokens[idx] = &looprpc.L402Token{
			BaseMacaroon:       macBytes,
			PaymentHash:        token.PaymentHash[:],
			PaymentPreimage:    token.Preimage[:],
			AmountPaidMsat:     int64(token.AmountPaid),
			RoutingFeePaidMsat: int64(token.RoutingFeePaid),
			TimeCreated:        token.TimeCreated.Unix(),
			Expired:            !token.IsValid(),
			StorageName:        key,
			Id: hex.EncodeToString(
				id.TokenID[:],
			),
		}
		idx++
	}

	return &looprpc.TokensResponse{Tokens: rpcTokens}, nil
}

// GetLsatTokens returns all tokens that are contained in the L402 token store.
// Deprecated: use GetL402Tokens.
// This API is provided to maintain backward compatibility with gRPC clients
// (e.g. `loop listauth`, Terminal Web, RTL).
// Type LsatToken used by GetLsatTokens in the past was renamed to L402Token,
// but this does not affect binary encoding, so we can use type L402Token here.
func (s *swapClientServer) GetLsatTokens(ctx context.Context,
	req *looprpc.TokensRequest) (*looprpc.TokensResponse, error) {

	warnf("Received deprecated call GetLsatTokens. Please update the " +
		"client software. Calling GetL402Tokens now.")

	return s.GetL402Tokens(ctx, req)
}

// FetchL402Token fetches a L402 Token from the server. This is required to
// listen for server notifications such as reservations. If a token is already
// in the local L402, nothing will happen.
func (s *swapClientServer) FetchL402Token(ctx context.Context,
	_ *looprpc.FetchL402TokenRequest) (*looprpc.FetchL402TokenResponse,
	error) {

	err := s.impl.Server.FetchL402(ctx)
	if err != nil {
		return nil, err
	}

	return &looprpc.FetchL402TokenResponse{}, nil
}

// GetInfo returns basic information about the loop daemon and details to swaps
// from the swap store.
func (s *swapClientServer) GetInfo(ctx context.Context,
	_ *looprpc.GetInfoRequest) (*looprpc.GetInfoResponse, error) {

	// Fetch loop-outs from the loop db.
	outSwaps, err := s.impl.Store.FetchLoopOutSwaps(ctx)
	if err != nil {
		return nil, err
	}

	// Collect loop-out stats.
	loopOutStats := &looprpc.LoopStats{}
	for _, out := range outSwaps {
		switch out.State().State.Type() {
		case loopdb.StateTypeSuccess:
			loopOutStats.SuccessCount++
			loopOutStats.SumSucceededAmt += int64(
				out.Contract.AmountRequested,
			)

		case loopdb.StateTypePending:
			loopOutStats.PendingCount++
			loopOutStats.SumPendingAmt += int64(
				out.Contract.AmountRequested,
			)

		case loopdb.StateTypeFail:
			loopOutStats.FailCount++
		}
	}

	// Fetch loop-ins from the loop db.
	inSwaps, err := s.impl.Store.FetchLoopInSwaps(ctx)
	if err != nil {
		return nil, err
	}

	// Collect loop-in stats.
	loopInStats := &looprpc.LoopStats{}
	for _, in := range inSwaps {
		switch in.State().State.Type() {
		case loopdb.StateTypeSuccess:
			loopInStats.SuccessCount++
			loopInStats.SumSucceededAmt += int64(
				in.Contract.AmountRequested,
			)

		case loopdb.StateTypePending:
			loopInStats.PendingCount++
			loopInStats.SumPendingAmt += int64(
				in.Contract.AmountRequested,
			)

		case loopdb.StateTypeFail:
			loopInStats.FailCount++
		}
	}

	commitHash := loop.CommitHash
	if loop.Dirty != "" {
		// If the build was dirty, we add a "-dirty" suffix to the
		// commit hash.
		commitHash += "-" + loop.Dirty
	}

	return &looprpc.GetInfoResponse{
		Version:      loop.Version(),
		CommitHash:   commitHash,
		Network:      s.config.Network,
		RpcListen:    s.config.RPCListen,
		RestListen:   s.config.RESTListen,
		MacaroonPath: s.config.MacaroonPath,
		TlsCertPath:  s.config.TLSCertPath,
		LoopOutStats: loopOutStats,
		LoopInStats:  loopInStats,
	}, nil
}

// StopDaemon triggers a graceful shutdown of the daemon process.
func (s *swapClientServer) StopDaemon(ctx context.Context,
	_ *looprpc.StopDaemonRequest) (*looprpc.StopDaemonResponse, error) {

	// Ensure we have a shutdown handler to invoke.
	if s.stopDaemon == nil {
		return nil, status.Error(codes.Unimplemented,
			"stop daemon not supported")
	}

	// Initiate the shutdown sequence.
	s.stopDaemon()

	return &looprpc.StopDaemonResponse{}, nil
}

// SweepHtlc spends a Loop HTLC output using the success path and a known
// preimage.
func (s *swapClientServer) SweepHtlc(ctx context.Context,
	req *looprpc.SweepHtlcRequest) (*looprpc.SweepHtlcResponse, error) {

	return sweepHtlc(
		ctx, req, s.lnd.ChainParams, s.impl.Store,
		s.lnd.ChainNotifier, s.lnd.WalletKit, s.lnd.Signer,
	)
}

// GetLiquidityParams gets our current liquidity manager's parameters.
func (s *swapClientServer) GetLiquidityParams(_ context.Context,
	_ *looprpc.GetLiquidityParamsRequest) (*looprpc.LiquidityParameters,
	error) {

	cfg := s.liquidityMgr.GetParameters()

	rpcCfg, err := liquidity.ParametersToRpc(cfg)
	if err != nil {
		return nil, err
	}

	return rpcCfg, nil
}

// SetLiquidityParams attempts to set our current liquidity manager's
// parameters.
func (s *swapClientServer) SetLiquidityParams(ctx context.Context,
	in *looprpc.SetLiquidityParamsRequest) (*looprpc.SetLiquidityParamsResponse,
	error) {

	err := s.liquidityMgr.SetParameters(ctx, in.Parameters)
	if err != nil {
		return nil, err
	}

	return &looprpc.SetLiquidityParamsResponse{}, nil
}

// SuggestSwaps provides a list of suggested swaps based on lnd's current
// channel balances and rules set by the liquidity manager.
func (s *swapClientServer) SuggestSwaps(ctx context.Context,
	_ *looprpc.SuggestSwapsRequest) (*looprpc.SuggestSwapsResponse, error) {

	suggestions, err := s.liquidityMgr.SuggestSwaps(ctx)
	switch err {
	case liquidity.ErrNoRules:
		return nil, status.Error(codes.FailedPrecondition, err.Error())

	case nil:

	default:
		return nil, err
	}

	resp := &looprpc.SuggestSwapsResponse{
		LoopOut: make(
			[]*looprpc.LoopOutRequest, len(suggestions.OutSwaps),
		),
		LoopIn: make(
			[]*looprpc.LoopInRequest, len(suggestions.InSwaps),
		),
	}

	for i, swap := range suggestions.OutSwaps {
		resp.LoopOut[i] = &looprpc.LoopOutRequest{
			Amt:                 int64(swap.Amount),
			OutgoingChanSet:     swap.OutgoingChanSet,
			MaxSwapFee:          int64(swap.MaxSwapFee),
			MaxMinerFee:         int64(swap.MaxMinerFee),
			MaxPrepayAmt:        int64(swap.MaxPrepayAmount),
			MaxSwapRoutingFee:   int64(swap.MaxSwapRoutingFee),
			MaxPrepayRoutingFee: int64(swap.MaxPrepayRoutingFee),
			SweepConfTarget:     swap.SweepConfTarget,
		}
	}

	for i, swap := range suggestions.InSwaps {
		loopIn := &looprpc.LoopInRequest{
			Amt:            int64(swap.Amount),
			MaxSwapFee:     int64(swap.MaxSwapFee),
			MaxMinerFee:    int64(swap.MaxMinerFee),
			HtlcConfTarget: swap.HtlcConfTarget,
		}

		if swap.LastHop != nil {
			loopIn.LastHop = swap.LastHop[:]
		}

		resp.LoopIn[i] = loopIn
	}

	for id, reason := range suggestions.DisqualifiedChans {
		autoloopReason, err := rpcAutoloopReason(reason)
		if err != nil {
			return nil, err
		}

		exclChan := &looprpc.Disqualified{
			Reason:    autoloopReason,
			ChannelId: id.ToUint64(),
		}

		resp.Disqualified = append(resp.Disqualified, exclChan)
	}

	for pubkey, reason := range suggestions.DisqualifiedPeers {
		autoloopReason, err := rpcAutoloopReason(reason)
		if err != nil {
			return nil, err
		}

		clonedPubkey := route.Vertex{}
		copy(clonedPubkey[:], pubkey[:])

		exclChan := &looprpc.Disqualified{
			Reason: autoloopReason,
			Pubkey: clonedPubkey[:],
		}

		resp.Disqualified = append(resp.Disqualified, exclChan)
	}

	return resp, nil
}

// ListReservations lists all existing reservations the client has ever made.
func (s *swapClientServer) ListReservations(ctx context.Context,
	_ *looprpc.ListReservationsRequest) (
	*looprpc.ListReservationsResponse, error) {

	if s.reservationManager == nil {
		return nil, status.Error(codes.Unimplemented,
			"Restart loop with --experimental")
	}
	reservations, err := s.reservationManager.GetReservations(
		ctx,
	)
	if err != nil {
		return nil, err
	}

	return &looprpc.ListReservationsResponse{
		Reservations: ToClientReservations(
			reservations,
		),
	}, nil
}

// InstantOut initiates an instant out swap.
func (s *swapClientServer) InstantOut(ctx context.Context,
	req *looprpc.InstantOutRequest) (*looprpc.InstantOutResponse,
	error) {

	if s.instantOutManager == nil {
		return nil, status.Error(codes.Unimplemented,
			"Restart loop with --experimental")
	}

	reservationIds := make([]reservation.ID, len(req.ReservationIds))
	for i, id := range req.ReservationIds {
		if len(id) != reservation.IdLength {
			return nil, fmt.Errorf("invalid reservation id: "+
				"expected %v bytes, got %d",
				reservation.IdLength, len(id))
		}

		var resId reservation.ID
		copy(resId[:], id)

		reservationIds[i] = resId
	}

	instantOutFsm, err := s.instantOutManager.NewInstantOut(
		ctx, reservationIds, req.DestAddr,
	)
	if err != nil {
		return nil, err
	}

	res := &looprpc.InstantOutResponse{
		InstantOutHash: instantOutFsm.InstantOut.SwapHash[:],
		State:          string(instantOutFsm.InstantOut.State),
	}

	if instantOutFsm.InstantOut.SweepTxHash != nil {
		res.SweepTxId = instantOutFsm.InstantOut.SweepTxHash.String()
	}

	return res, nil
}

// InstantOutQuote returns a quote for an instant out swap with the provided
// parameters.
func (s *swapClientServer) InstantOutQuote(ctx context.Context,
	req *looprpc.InstantOutQuoteRequest) (
	*looprpc.InstantOutQuoteResponse, error) {

	if s.instantOutManager == nil {
		return nil, status.Error(codes.Unimplemented,
			"Restart loop with --experimental")
	}

	quote, err := s.instantOutManager.GetInstantOutQuote(
		ctx, btcutil.Amount(req.Amt), req.ReservationIds,
	)
	if err != nil {
		return nil, err
	}

	return &looprpc.InstantOutQuoteResponse{
		ServiceFeeSat: int64(quote.ServiceFee),
		SweepFeeSat:   int64(quote.OnChainFee),
	}, nil
}

// ListInstantOuts returns a list of all currently known instant out swaps and
// their current status.
func (s *swapClientServer) ListInstantOuts(ctx context.Context,
	_ *looprpc.ListInstantOutsRequest) (
	*looprpc.ListInstantOutsResponse, error) {

	if s.instantOutManager == nil {
		return nil, status.Error(codes.Unimplemented,
			"Restart loop with --experimental")
	}

	instantOuts, err := s.instantOutManager.ListInstantOuts(ctx)
	if err != nil {
		return nil, err
	}

	rpcSwaps := make([]*looprpc.InstantOut, 0, len(instantOuts))
	for _, instantOut := range instantOuts {
		rpcSwaps = append(rpcSwaps, rpcInstantOut(instantOut))
	}

	return &looprpc.ListInstantOutsResponse{
		Swaps: rpcSwaps,
	}, nil
}

func rpcInstantOut(instantOut *instantout.InstantOut) *looprpc.InstantOut {
	var sweepTxId string
	if instantOut.SweepTxHash != nil {
		sweepTxId = instantOut.SweepTxHash.String()
	}

	reservations := make([][]byte, len(instantOut.Reservations))
	for i, res := range instantOut.Reservations {
		reservations[i] = res.ID[:]
	}

	return &looprpc.InstantOut{
		SwapHash:       instantOut.SwapHash[:],
		State:          string(instantOut.State),
		Amount:         uint64(instantOut.Value),
		SweepTxId:      sweepTxId,
		ReservationIds: reservations,
	}
}

// NewStaticAddress is the rpc endpoint for loop clients to request a new static
// address.
func (s *swapClientServer) NewStaticAddress(ctx context.Context,
	_ *looprpc.NewStaticAddressRequest) (
	*looprpc.NewStaticAddressResponse, error) {

	staticAddress, expiry, err := s.staticAddressManager.NewAddress(ctx)
	if err != nil {
		return nil, err
	}

	return &looprpc.NewStaticAddressResponse{
		Address: staticAddress.String(),
		Expiry:  uint32(expiry),
	}, nil
}

// ListUnspentDeposits returns a list of utxos behind the static address.
func (s *swapClientServer) ListUnspentDeposits(ctx context.Context,
	req *looprpc.ListUnspentDepositsRequest) (
	*looprpc.ListUnspentDepositsResponse, error) {

	// List all unspent utxos the wallet sees, regardless of the number of
	// confirmations.
	staticAddress, utxos, err := s.staticAddressManager.ListUnspentRaw(
		ctx, req.MinConfs, req.MaxConfs,
	)
	if err != nil {
		return nil, err
	}

	// ListUnspentRaw returns the unspent wallet view of the backing lnd
	// wallet. It might be that deposits show up there that are actually
	// not spendable because they already have been used but not yet spent
	// by the server. We filter out such deposits here.
	var (
		outpoints []string
		isUnspent = make(map[wire.OutPoint]struct{})
	)

	// Keep track of confirmed outpoints that we need to check against our
	// database.
	confirmedToCheck := make(map[wire.OutPoint]struct{})

	for _, utxo := range utxos {
		if utxo.Confirmations < deposit.MinConfs {
			// Unconfirmed deposits are always available.
			isUnspent[utxo.OutPoint] = struct{}{}
		} else {
			// Confirmed deposits need to be checked.
			outpoints = append(outpoints, utxo.OutPoint.String())
			confirmedToCheck[utxo.OutPoint] = struct{}{}
		}
	}

	// Check the spent status of the deposits by looking at their states.
	ignoreUnknownOutpoints := false
	deposits, err := s.depositManager.DepositsForOutpoints(
		ctx, outpoints, ignoreUnknownOutpoints,
	)
	if err != nil {
		return nil, err
	}
	for _, d := range deposits {
		// A nil deposit means we don't have a record for it. We'll
		// handle this case after the loop.
		if d == nil {
			continue
		}

		// If the deposit is in the "Deposited" state, it's available.
		if d.IsInState(deposit.Deposited) {
			isUnspent[d.OutPoint] = struct{}{}
		}

		// We have a record for this deposit, so we no longer need to
		// check it.
		delete(confirmedToCheck, d.OutPoint)
	}

	// Any remaining outpoints in confirmedToCheck are ones that lnd knows
	// about but we don't. These are new, unspent deposits.
	for op := range confirmedToCheck {
		isUnspent[op] = struct{}{}
	}

	// Prepare the list of unspent deposits for the rpc response.
	var respUtxos []*looprpc.Utxo
	for _, u := range utxos {
		if _, ok := isUnspent[u.OutPoint]; !ok {
			continue
		}

		utxo := &looprpc.Utxo{
			StaticAddress: staticAddress.String(),
			AmountSat:     int64(u.Value),
			Confirmations: u.Confirmations,
			Outpoint:      u.OutPoint.String(),
		}
		respUtxos = append(respUtxos, utxo)
	}

	return &looprpc.ListUnspentDepositsResponse{Utxos: respUtxos}, nil
}

// WithdrawDeposits tries to obtain a partial signature from the server to spend
// the selected deposits to the client's wallet.
func (s *swapClientServer) WithdrawDeposits(ctx context.Context,
	req *looprpc.WithdrawDepositsRequest) (
	*looprpc.WithdrawDepositsResponse, error) {

	var (
		isAllSelected  = req.All
		isUtxoSelected = len(req.Outpoints) > 0
		outpoints      []wire.OutPoint
		err            error
	)

	switch {
	case isAllSelected == isUtxoSelected:
		return nil, fmt.Errorf("must select either all or some utxos")

	case isAllSelected:
		deposits, err := s.depositManager.GetActiveDepositsInState(
			deposit.Deposited,
		)
		if err != nil {
			return nil, err
		}

		for _, d := range deposits {
			outpoints = append(outpoints, d.OutPoint)
		}

	case isUtxoSelected:
		outpoints, err = staticutil.ToWireOutpoints(req.Outpoints)
		if err != nil {
			return nil, err
		}
	}

	txhash, address, err := s.withdrawalManager.DeliverWithdrawalRequest(
		ctx, outpoints, req.DestAddr, req.SatPerVbyte, req.Amount,
	)
	if err != nil {
		return nil, err
	}

	return &looprpc.WithdrawDepositsResponse{
		WithdrawalTxHash: txhash,
		Address:          address,
	}, err
}

// ListStaticAddressDeposits returns a list of all sufficiently confirmed
// deposits behind the static address and displays properties like value,
// state or blocks til expiry.
func (s *swapClientServer) ListStaticAddressDeposits(ctx context.Context,
	req *looprpc.ListStaticAddressDepositsRequest) (
	*looprpc.ListStaticAddressDepositsResponse, error) {

	outpoints := req.Outpoints
	if req.StateFilter != looprpc.DepositState_UNKNOWN_STATE &&
		len(outpoints) > 0 {

		return nil, fmt.Errorf("can either filter by state or " +
			"outpoints")
	}

	allDeposits, err := s.depositManager.GetAllDeposits(ctx)
	if err != nil {
		return nil, err
	}

	// Deposits filtered by state or outpoints.
	var filteredDeposits []*looprpc.Deposit
	if len(outpoints) > 0 {
		f := func(d *deposit.Deposit) bool {
			for _, outpoint := range outpoints {
				if outpoint == d.OutPoint.String() {
					return true
				}
			}
			return false
		}
		filteredDeposits = filter(allDeposits, f)

		if len(outpoints) != len(filteredDeposits) {
			return nil, fmt.Errorf("not all outpoints found in " +
				"deposits")
		}
	} else {
		f := func(d *deposit.Deposit) bool {
			if req.StateFilter == looprpc.DepositState_UNKNOWN_STATE {
				// Per default, we return deposits in all
				// states.
				return true
			}

			return d.IsInState(toServerState(req.StateFilter))
		}
		filteredDeposits = filter(allDeposits, f)
	}

	// Calculate the blocks until expiry for each deposit.
	err = s.populateBlocksUntilExpiry(ctx, filteredDeposits)
	if err != nil {
		infof("Failed to populate blocks until expiry: %v", err)
	}

	return &looprpc.ListStaticAddressDepositsResponse{
		FilteredDeposits: filteredDeposits,
	}, nil
}

// ListStaticAddressWithdrawals returns a list of all finalized withdrawal
// transactions.
func (s *swapClientServer) ListStaticAddressWithdrawals(ctx context.Context,
	_ *looprpc.ListStaticAddressWithdrawalRequest) (
	*looprpc.ListStaticAddressWithdrawalResponse, error) {

	withdrawals, err := s.withdrawalManager.GetAllWithdrawals(ctx)
	if err != nil {
		return nil, err
	}

	if len(withdrawals) == 0 {
		return &looprpc.ListStaticAddressWithdrawalResponse{}, nil
	}

	clientWithdrawals := make(
		[]*looprpc.StaticAddressWithdrawal, 0, len(withdrawals),
	)
	for _, w := range withdrawals {
		deposits := make([]*looprpc.Deposit, 0, len(w.Deposits))
		for _, d := range w.Deposits {
			deposits = append(deposits, &looprpc.Deposit{
				Id:                 d.ID[:],
				Outpoint:           d.OutPoint.String(),
				Value:              int64(d.Value),
				ConfirmationHeight: d.ConfirmationHeight,
				State: toClientDepositState(
					d.GetState(),
				),
			})
		}
		withdrawal := &looprpc.StaticAddressWithdrawal{
			TxId:                       w.TxID.String(),
			Deposits:                   deposits,
			TotalDepositAmountSatoshis: int64(w.TotalDepositAmount),
			WithdrawnAmountSatoshis:    int64(w.WithdrawnAmount),
			ChangeAmountSatoshis:       int64(w.ChangeAmount),
			ConfirmationHeight:         uint32(w.ConfirmationHeight),
		}
		clientWithdrawals = append(clientWithdrawals, withdrawal)
	}

	return &looprpc.ListStaticAddressWithdrawalResponse{
		Withdrawals: clientWithdrawals,
	}, nil
}

// ListStaticAddressSwaps returns a list of all swaps that are currently pending
// or previously succeeded.
func (s *swapClientServer) ListStaticAddressSwaps(ctx context.Context,
	_ *looprpc.ListStaticAddressSwapsRequest) (
	*looprpc.ListStaticAddressSwapsResponse, error) {

	swaps, err := s.staticLoopInManager.GetAllSwaps(ctx)
	if err != nil {
		return nil, err
	}

	if len(swaps) == 0 {
		return &looprpc.ListStaticAddressSwapsResponse{}, nil
	}

	// Query lnd's info to get the current block height.
	lndInfo, err := s.lnd.Client.GetInfo(ctx)
	if err != nil {
		return nil, err
	}

	addrParams, err := s.staticAddressManager.GetStaticAddressParameters(
		ctx,
	)
	if err != nil {
		return nil, err
	}

	// Fetch all deposits at once and index them by swap hash for a quick
	// lookup.
	allDeposits, err := s.depositManager.GetAllDeposits(ctx)
	if err != nil {
		return nil, err
	}

	depositsBySwap := make(map[lntypes.Hash][]*deposit.Deposit, len(swaps))
	for _, d := range allDeposits {
		if d.SwapHash == nil {
			// This deposit is not associated with a swap, so we
			// skip it.
			continue
		}
		depositsBySwap[*d.SwapHash] = append(
			depositsBySwap[*d.SwapHash], d,
		)
	}

	var clientSwaps []*looprpc.StaticAddressLoopInSwap
	for _, swp := range swaps {
		chainParams, err := s.network.ChainParams()
		if err != nil {
			return nil, fmt.Errorf("error getting chain params")
		}
		swapPayReq, err := zpay32.Decode(swp.SwapInvoice, chainParams)
		if err != nil {
			return nil, fmt.Errorf("error decoding swap "+
				"invoice: %v", err)
		}

		// Assemble the deposits associated with this swap, if any.
		var protoDeposits []*looprpc.Deposit
		if ds, ok := depositsBySwap[swp.SwapHash]; ok {
			protoDeposits = make([]*looprpc.Deposit, 0, len(ds))
			for _, d := range ds {
				state := toClientDepositState(d.GetState())
				blocksUntilExpiry := d.ConfirmationHeight +
					int64(addrParams.Expiry) -
					int64(lndInfo.BlockHeight)

				pd := &looprpc.Deposit{
					Id:                 d.ID[:],
					State:              state,
					Outpoint:           d.OutPoint.String(),
					Value:              int64(d.Value),
					ConfirmationHeight: d.ConfirmationHeight,
					SwapHash:           d.SwapHash[:],
					BlocksUntilExpiry:  blocksUntilExpiry,
				}
				protoDeposits = append(protoDeposits, pd)
			}
		}

		swapAmount := swp.TotalDepositAmount()
		state := toClientStaticAddressLoopInState(swp.GetState())
		payReqAmount := int64(swapPayReq.MilliSat.ToSatoshis())
		if swp.SelectedAmount > 0 {
			swapAmount = swp.SelectedAmount
		}
		swap := &looprpc.StaticAddressLoopInSwap{
			SwapHash:                     swp.SwapHash[:],
			DepositOutpoints:             swp.DepositOutpoints,
			State:                        state,
			SwapAmountSatoshis:           int64(swapAmount),
			PaymentRequestAmountSatoshis: payReqAmount,
			Deposits:                     protoDeposits,
		}

		clientSwaps = append(clientSwaps, swap)
	}

	return &looprpc.ListStaticAddressSwapsResponse{
		Swaps: clientSwaps,
	}, nil
}

// GetStaticAddressSummary returns a summary of static address-related
// information. Amongst deposits and withdrawals and their total values, it also
// includes a list of detailed deposit information filtered by their state.
func (s *swapClientServer) GetStaticAddressSummary(ctx context.Context,
	_ *looprpc.StaticAddressSummaryRequest) (
	*looprpc.StaticAddressSummaryResponse, error) {

	allDeposits, err := s.depositManager.GetAllDeposits(ctx)
	if err != nil {
		return nil, err
	}

	var (
		totalNumDeposits    = len(allDeposits)
		valueUnconfirmed    int64
		valueDeposited      int64
		valueExpired        int64
		valueWithdrawn      int64
		valueLoopedIn       int64
		valueChannelsOpened int64
		htlcTimeoutSwept    int64
	)

	// Value unconfirmed.
	utxos, err := s.staticAddressManager.ListUnspent(
		ctx, 0, deposit.MinConfs-1,
	)
	if err != nil {
		return nil, err
	}
	for _, u := range utxos {
		valueUnconfirmed += int64(u.Value)
	}

	// Confirmed total values by category.
	for _, d := range allDeposits {
		value := int64(d.Value)
		switch d.GetState() {
		case deposit.Deposited:
			valueDeposited += value

		case deposit.Expired:
			valueExpired += value

		case deposit.Withdrawn:
			valueWithdrawn += value

		case deposit.LoopedIn:
			valueLoopedIn += value

		case deposit.HtlcTimeoutSwept:
			htlcTimeoutSwept += value

		case deposit.ChannelPublished:
			valueChannelsOpened += value
		}
	}

	params, err := s.staticAddressManager.GetStaticAddressParameters(ctx)
	if err != nil {
		return nil, err
	}

	address, err := s.staticAddressManager.GetTaprootAddress(
		params.ClientPubkey, params.ServerPubkey, int64(params.Expiry),
	)
	if err != nil {
		return nil, err
	}

	return &looprpc.StaticAddressSummaryResponse{
		StaticAddress:                  address.String(),
		RelativeExpiryBlocks:           uint64(params.Expiry),
		TotalNumDeposits:               uint32(totalNumDeposits),
		ValueUnconfirmedSatoshis:       valueUnconfirmed,
		ValueDepositedSatoshis:         valueDeposited,
		ValueExpiredSatoshis:           valueExpired,
		ValueWithdrawnSatoshis:         valueWithdrawn,
		ValueLoopedInSatoshis:          valueLoopedIn,
		ValueChannelsOpened:            valueChannelsOpened,
		ValueHtlcTimeoutSweepsSatoshis: htlcTimeoutSwept,
	}, nil
}

// StaticAddressLoopIn initiates a loop-in request using static address
// deposits.
func (s *swapClientServer) StaticAddressLoopIn(ctx context.Context,
	in *looprpc.StaticAddressLoopInRequest) (
	*looprpc.StaticAddressLoopInResponse, error) {

	infof("Static loop-in request received")

	routeHints, err := unmarshallRouteHints(in.RouteHints)
	if err != nil {
		return nil, err
	}

	req := &loop.StaticAddressLoopInRequest{
		SelectedAmount:        btcutil.Amount(in.Amount),
		DepositOutpoints:      in.Outpoints,
		MaxSwapFee:            btcutil.Amount(in.MaxSwapFeeSatoshis),
		Label:                 in.Label,
		Initiator:             in.Initiator,
		Private:               in.Private,
		RouteHints:            routeHints,
		PaymentTimeoutSeconds: in.PaymentTimeoutSeconds,
		Fast:                  in.Fast,
	}

	if in.LastHop != nil {
		lastHop, err := route.NewVertexFromBytes(in.LastHop)
		if err != nil {
			return nil, err
		}
		req.LastHop = &lastHop
	}

	loopIn, err := s.staticLoopInManager.DeliverLoopInRequest(ctx, req)
	if err != nil {
		return nil, err
	}

	// Build a list of used deposits for the response.
	usedDeposits := filter(
		loopIn.Deposits, func(d *deposit.Deposit) bool { return true },
	)

	err = s.populateBlocksUntilExpiry(ctx, usedDeposits)
	if err != nil {
		infof("Failed to populate blocks until expiry: %v", err)
	}

	// Determine the actual swap amount and change based on the selected
	// amount and the total value of the selected deposits.
	total := loopIn.TotalDepositAmount()
	swapAmt := total
	var changeAmt btcutil.Amount
	if loopIn.SelectedAmount > 0 {
		amt, err := loopin.DeduceSwapAmount(
			total, loopIn.SelectedAmount,
		)
		if err == nil {
			swapAmt = amt
			changeAmt = total - amt
		}
	}

	return &looprpc.StaticAddressLoopInResponse{
		SwapHash:              loopIn.SwapHash[:],
		State:                 string(loopIn.GetState()),
		Amount:                uint64(total),
		SwapAmount:            uint64(swapAmt),
		Change:                int64(changeAmt),
		QuotedSwapFeeSatoshis: int64(loopIn.QuotedSwapFee),
		Fast:                  loopIn.Fast,
		HtlcCltv:              loopIn.HtlcCltvExpiry,
		MaxSwapFeeSatoshis:    int64(loopIn.MaxSwapFee),
		InitiationHeight:      loopIn.InitiationHeight,
		ProtocolVersion:       loopIn.ProtocolVersion.String(),
		Initiator:             loopIn.Initiator,
		Label:                 loopIn.Label,
		PaymentTimeoutSeconds: loopIn.PaymentTimeoutSeconds,
		UsedDeposits:          usedDeposits,
	}, nil
}

// Calculate the blocks until expiry for each deposit and return the modified
// StaticAddressLoopInResponse.
func (s *swapClientServer) populateBlocksUntilExpiry(ctx context.Context,
	deposits []*looprpc.Deposit) error {

	lndInfo, err := s.lnd.Client.GetInfo(ctx)
	if err != nil {
		return err
	}

	bestBlockHeight := int64(lndInfo.BlockHeight)
	params, err := s.staticAddressManager.GetStaticAddressParameters(ctx)
	if err != nil {
		return err
	}
	for i := 0; i < len(deposits); i++ {
		deposits[i].BlocksUntilExpiry =
			deposits[i].ConfirmationHeight +
				int64(params.Expiry) - bestBlockHeight
	}
	return nil
}

// StaticOpenChannel initiates an open channel request using static address
// deposits.
func (s *swapClientServer) StaticOpenChannel(ctx context.Context,
	req *looprpc.StaticOpenChannelRequest) (*looprpc.StaticOpenChannelResponse,
	error) {

	infof("Static open channel request received")

	if req == nil || req.OpenChannelRequest == nil {
		return &looprpc.StaticOpenChannelResponse{},
			fmt.Errorf("missing open channel request")
	}

	chanOutpoint, err := s.openChannelManager.DeliverOpenChannelRequest(
		ctx, req.OpenChannelRequest,
	)

	var outpointStr string
	if chanOutpoint != nil {
		outpointStr = chanOutpoint.String()
	}

	return &looprpc.StaticOpenChannelResponse{
		ChannelOpenOutpoint: outpointStr,
	}, err
}

type filterFunc func(deposits *deposit.Deposit) bool

func filter(deposits []*deposit.Deposit, f filterFunc) []*looprpc.Deposit {
	var clientDeposits []*looprpc.Deposit
	for _, d := range deposits {
		if !f(d) {
			continue
		}

		swapHash := make([]byte, 0, len(lntypes.Hash{}))
		if d.SwapHash != nil {
			swapHash = d.SwapHash[:]
		}

		hash := d.Hash
		outpoint := wire.NewOutPoint(&hash, d.Index).String()
		deposit := &looprpc.Deposit{
			Id: d.ID[:],
			State: toClientDepositState(
				d.GetState(),
			),
			Outpoint:           outpoint,
			Value:              int64(d.Value),
			ConfirmationHeight: d.ConfirmationHeight,
			SwapHash:           swapHash,
		}

		clientDeposits = append(clientDeposits, deposit)
	}

	return clientDeposits
}

func toClientDepositState(state fsm.StateType) looprpc.DepositState {
	switch state {
	case deposit.Deposited:
		return looprpc.DepositState_DEPOSITED

	case deposit.Withdrawing:
		return looprpc.DepositState_WITHDRAWING

	case deposit.Withdrawn:
		return looprpc.DepositState_WITHDRAWN

	case deposit.PublishExpirySweep:
		return looprpc.DepositState_PUBLISH_EXPIRED

	case deposit.LoopingIn:
		return looprpc.DepositState_LOOPING_IN

	case deposit.LoopedIn:
		return looprpc.DepositState_LOOPED_IN

	case deposit.OpeningChannel:
		return looprpc.DepositState_OPENING_CHANNEL

	case deposit.ChannelPublished:
		return looprpc.DepositState_CHANNEL_PUBLISHED

	case deposit.SweepHtlcTimeout:
		return looprpc.DepositState_SWEEP_HTLC_TIMEOUT

	case deposit.HtlcTimeoutSwept:
		return looprpc.DepositState_HTLC_TIMEOUT_SWEPT

	case deposit.WaitForExpirySweep:
		return looprpc.DepositState_WAIT_FOR_EXPIRY_SWEEP

	case deposit.Expired:
		return looprpc.DepositState_EXPIRED

	default:
		return looprpc.DepositState_UNKNOWN_STATE
	}
}

func toClientStaticAddressLoopInState(
	state fsm.StateType) looprpc.StaticAddressLoopInSwapState {

	switch state {
	case loopin.InitHtlcTx:
		return looprpc.StaticAddressLoopInSwapState_INIT_HTLC

	case loopin.SignHtlcTx:
		return looprpc.StaticAddressLoopInSwapState_SIGN_HTLC_TX

	case loopin.MonitorInvoiceAndHtlcTx:
		return looprpc.StaticAddressLoopInSwapState_MONITOR_INVOICE_HTLC_TX

	case loopin.PaymentReceived:
		return looprpc.StaticAddressLoopInSwapState_PAYMENT_RECEIVED

	case loopin.SweepHtlcTimeout:
		return looprpc.StaticAddressLoopInSwapState_SWEEP_STATIC_ADDRESS_HTLC_TIMEOUT

	case loopin.MonitorHtlcTimeoutSweep:
		return looprpc.StaticAddressLoopInSwapState_MONITOR_HTLC_TIMEOUT_SWEEP

	case loopin.HtlcTimeoutSwept:
		return looprpc.StaticAddressLoopInSwapState_HTLC_STATIC_ADDRESS_TIMEOUT_SWEPT

	case loopin.Succeeded:
		return looprpc.StaticAddressLoopInSwapState_SUCCEEDED

	case loopin.SucceededTransitioningFailed:
		return looprpc.StaticAddressLoopInSwapState_SUCCEEDED_TRANSITIONING_FAILED

	case loopin.UnlockDeposits:
		return looprpc.StaticAddressLoopInSwapState_UNLOCK_DEPOSITS

	case loopin.Failed:
		return looprpc.StaticAddressLoopInSwapState_FAILED_STATIC_ADDRESS_SWAP

	default:
		return looprpc.StaticAddressLoopInSwapState_UNKNOWN_STATIC_ADDRESS_SWAP_STATE
	}
}

func toServerState(state looprpc.DepositState) fsm.StateType {
	switch state {
	case looprpc.DepositState_DEPOSITED:
		return deposit.Deposited

	case looprpc.DepositState_WITHDRAWING:
		return deposit.Withdrawing

	case looprpc.DepositState_WITHDRAWN:
		return deposit.Withdrawn

	case looprpc.DepositState_PUBLISH_EXPIRED:
		return deposit.PublishExpirySweep

	case looprpc.DepositState_LOOPING_IN:
		return deposit.LoopingIn

	case looprpc.DepositState_LOOPED_IN:
		return deposit.LoopedIn

	case looprpc.DepositState_OPENING_CHANNEL:
		return deposit.OpeningChannel

	case looprpc.DepositState_CHANNEL_PUBLISHED:
		return deposit.ChannelPublished

	case looprpc.DepositState_SWEEP_HTLC_TIMEOUT:
		return deposit.SweepHtlcTimeout

	case looprpc.DepositState_HTLC_TIMEOUT_SWEPT:
		return deposit.HtlcTimeoutSwept

	case looprpc.DepositState_WAIT_FOR_EXPIRY_SWEEP:
		return deposit.WaitForExpirySweep

	case looprpc.DepositState_EXPIRED:
		return deposit.Expired

	default:
		return fsm.EmptyState
	}
}

func rpcAutoloopReason(reason liquidity.Reason) (looprpc.AutoReason, error) {
	switch reason {
	case liquidity.ReasonNone:
		return looprpc.AutoReason_AUTO_REASON_UNKNOWN, nil

	case liquidity.ReasonBudgetNotStarted:
		return looprpc.AutoReason_AUTO_REASON_BUDGET_NOT_STARTED, nil

	case liquidity.ReasonSweepFees:
		return looprpc.AutoReason_AUTO_REASON_SWEEP_FEES, nil

	case liquidity.ReasonBudgetElapsed:
		return looprpc.AutoReason_AUTO_REASON_BUDGET_ELAPSED, nil

	case liquidity.ReasonInFlight:
		return looprpc.AutoReason_AUTO_REASON_IN_FLIGHT, nil

	case liquidity.ReasonSwapFee:
		return looprpc.AutoReason_AUTO_REASON_SWAP_FEE, nil

	case liquidity.ReasonMinerFee:
		return looprpc.AutoReason_AUTO_REASON_MINER_FEE, nil

	case liquidity.ReasonPrepay:
		return looprpc.AutoReason_AUTO_REASON_PREPAY, nil

	case liquidity.ReasonFailureBackoff:
		return looprpc.AutoReason_AUTO_REASON_FAILURE_BACKOFF, nil

	case liquidity.ReasonLoopOut:
		return looprpc.AutoReason_AUTO_REASON_LOOP_OUT, nil

	case liquidity.ReasonLoopIn:
		return looprpc.AutoReason_AUTO_REASON_LOOP_IN, nil

	case liquidity.ReasonLiquidityOk:
		return looprpc.AutoReason_AUTO_REASON_LIQUIDITY_OK, nil

	case liquidity.ReasonBudgetInsufficient:
		return looprpc.AutoReason_AUTO_REASON_BUDGET_INSUFFICIENT, nil

	case liquidity.ReasonFeePPMInsufficient:
		return looprpc.AutoReason_AUTO_REASON_SWAP_FEE, nil

	default:
		return 0, fmt.Errorf("unknown autoloop reason: %v", reason)
	}
}

// processStatusUpdates reads updates on the status channel and processes them.
//
// NOTE: This must run inside a goroutine as it blocks until the main context
// shuts down.
func (s *swapClientServer) processStatusUpdates(mainCtx context.Context) {
	for {
		select {
		// On updates, refresh the server's in-memory state and inform
		// subscribers about the changes.
		case swp := <-s.statusChan:
			s.swapsLock.Lock()
			s.swaps[swp.SwapHash] = swp

			for _, subscriber := range s.subscribers {
				select {
				case subscriber <- swp:
				case <-mainCtx.Done():
					s.swapsLock.Unlock()
					return
				}
			}

			s.swapsLock.Unlock()

		// Server is shutting down.
		case <-mainCtx.Done():
			return
		}
	}
}

// validateConfTarget ensures the given confirmation target is valid. If one
// isn't specified (0 value), then the default target is used.
func validateConfTarget(target, defaultTarget int32) (int32, error) {
	switch {
	case target == 0:
		return defaultTarget, nil

	// Ensure the target respects our minimum threshold.
	case target < minConfTarget:
		return 0, fmt.Errorf("%w: A confirmation target of at "+
			"least %v must be provided", errConfTargetTooLow,
			minConfTarget)

	default:
		return target, nil
	}
}

// validateLoopInRequest fails if the mutually exclusive conf target and
// external parameters are both set. It returns the confirmation target of the
// legacy loop-in.
func validateLoopInRequest(htlcConfTarget int32, external bool,
	numDeposits uint32, amount btcutil.Amount,
	autoSelectDeposits bool) (int32, error) {

	if amount < 0 {
		return 0, errors.New("amount cannot be negative")
	}

	if amount == 0 && numDeposits == 0 {
		return 0, errors.New("either amount, or deposits or both " +
			"must be set")
	}

	if autoSelectDeposits && numDeposits > 0 {
		return 0, errors.New("cannot auto-select deposits while " +
			"providing deposits at the same time")
	}

	// If the htlc is going to be externally set, the htlcConfTarget should
	// not be set, because it has no relevance when the htlc is external.
	if external && htlcConfTarget != 0 {
		return 0, errors.New("external and htlc conf target cannot " +
			"both be set")
	}

	// If the htlc is being externally published, we do not need to set a
	// confirmation target.
	if external {
		return 0, nil
	}

	// If the loop in uses static address deposits, we do not need to set a
	// confirmation target since the HTLC won't be published by the client.
	if numDeposits > 0 || autoSelectDeposits {
		return 0, nil
	}

	return validateConfTarget(htlcConfTarget, loop.DefaultHtlcConfTarget)
}

// validateLoopOutRequest validates the confirmation target, destination
// address and label of the loop out request. It also checks that the requested
// loop amount is valid given the available balance.
func validateLoopOutRequest(ctx context.Context, lnd lndclient.LightningClient,
	chainParams *chaincfg.Params, req *looprpc.LoopOutRequest,
	sweepAddr btcutil.Address, maxParts uint32) (int32, error) {

	// Check that the provided destination address has the correct format
	// for the active network.
	if !sweepAddr.IsForNet(chainParams) {
		return 0, fmt.Errorf("%w: Current active network is %s",
			errIncorrectChain, chainParams.Name)
	}

	// Check that the provided destination address is a supported
	// address format.
	switch sweepAddr.(type) {
	case *btcutil.AddressTaproot,
		*btcutil.AddressWitnessScriptHash,
		*btcutil.AddressWitnessPubKeyHash,
		*btcutil.AddressScriptHash,
		*btcutil.AddressPubKeyHash:

	default:
		return 0, errInvalidAddress
	}

	// If this is an asset payment, we'll check that we have the necessary
	// outbound asset capacaity to fulfill the request.
	if req.AssetInfo != nil {
		// Todo(sputn1ck) actually check outbound capacity.
		return validateConfTarget(
			req.SweepConfTarget, loop.DefaultSweepConfTarget,
		)
	}

	// Check that the label is valid.
	if err := labels.Validate(req.Label); err != nil {
		return 0, err
	}

	channels, err := lnd.ListChannels(ctx, false, false)
	if err != nil {
		return 0, err
	}

	unlimitedChannels := len(req.OutgoingChanSet) == 0
	outgoingChanSetMap := make(map[uint64]bool)
	for _, chanID := range req.OutgoingChanSet {
		outgoingChanSetMap[chanID] = true
	}

	var activeChannelSet []lndclient.ChannelInfo
	for _, c := range channels {
		// Don't bother looking at inactive channels.
		if !c.Active {
			continue
		}

		// If no outgoing channel set was specified then all active
		// channels are considered. However, if a channel set was
		// specified then only the specified channels are considered.
		if unlimitedChannels || outgoingChanSetMap[c.ChannelID] {
			activeChannelSet = append(activeChannelSet, c)
		}
	}

	// Determine if the loop out request is theoretically possible given
	// the amount requested, the maximum possible routing fees,
	// the available channel set and the fact that equal splitting is
	// used for MPP.
	requiredBalance := btcutil.Amount(req.Amt + req.MaxSwapRoutingFee)
	isRoutable, _ := hasBandwidth(activeChannelSet, requiredBalance,
		int(maxParts))

	if !isRoutable {
		return 0, fmt.Errorf("%w: Requested swap amount of %d "+
			"sats along with the maximum routing fee of %d sats "+
			"is more than what can be routed given current state "+
			"of the channel set", errBalanceTooLow, req.Amt,
			req.MaxSwapRoutingFee)
	}

	return validateConfTarget(
		req.SweepConfTarget, loop.DefaultSweepConfTarget,
	)
}

// hasBandwidth simulates the MPP splitting logic that will be used by LND when
// attempting to route the payment. This function is used to evaluate if a
// payment will be routable given the splitting logic used by LND.
// It returns true if the amount is routable given the channel set and the
// maximum number of shards allowed. If the amount is routable then the number
// of shards used is also returned. This function makes an assumption that the
// minimum loop amount divided by max parts will not be less than the minimum
// shard amount. If the MPP logic changes, then this function should be updated.
func hasBandwidth(channels []lndclient.ChannelInfo, amt btcutil.Amount,
	maxParts int) (bool, int) {

	tracef("Checking if %v sats can be routed with %v parts over "+
		"channel set of length %v", amt, maxParts, len(channels))

	localBalances := make([]btcutil.Amount, len(channels))
	var totalBandwidth btcutil.Amount
	for i, channel := range channels {
		tracef("Channel %v: local=%v remote=%v", channel.ChannelID,
			channel.LocalBalance, channel.RemoteBalance)

		localBalances[i] = channel.LocalBalance
		totalBandwidth += channel.LocalBalance
	}

	tracef("Total bandwidth: %v", totalBandwidth)
	if totalBandwidth < amt {
		return false, 0
	}

	logLocalBalances := func(shard int) {
		tracef("Local balances for %v shards:", shard)
		for i, balance := range localBalances {
			tracef("Channel %v: localBalances[%v]=%v",
				channels[i].ChannelID, i, balance)
		}
	}

	split := amt
	for shard := 0; shard <= maxParts; {
		tracef("Trying to split %v sats into %v parts", amt, shard)

		paid := false
		for i := 0; i < len(localBalances); i++ {
			// TODO(hieblmi): Consider channel reserves because the
			//      channel can't send its full local balance.
			if localBalances[i] >= split {
				tracef("len(shards)=%v: Local channel "+
					"balance %v can pay %v sats",
					shard, localBalances[i], split)

				localBalances[i] -= split
				tracef("len(shards)=%v: Subtracted "+
					"%v sats from localBalance[%v]=%v",
					shard, split, i, localBalances[i])

				amt -= split
				tracef("len(shards)=%v: Remaining total "+
					"amount amt=%v", shard, amt)

				paid = true
				shard++

				break
			}
		}

		logLocalBalances(shard)

		if amt == 0 {
			tracef("Payment is routable with %v part(s)", shard)

			return true, shard
		}

		if !paid {
			tracef("len(shards)=%v: No channel could pay %v "+
				"sats, halving payment to %v and trying again",
				split/2)

			split /= 2
		} else {
			tracef("len(shards)=%v: Payment was made, trying "+
				"to pay remaining sats %v", shard, amt)

			split = amt
		}
	}

	tracef("Payment is not routable, remaining amount that can't be "+
		"sent: %v sats", amt)

	logLocalBalances(maxParts)

	return false, 0
}

// getPublicationDeadline returns the publication deadline for a swap given the
// unix timestamp. If the timestamp is believed to be in milliseconds, then it
// is converted to seconds.
func getPublicationDeadline(unixTimestamp uint64) time.Time {
	length := len(fmt.Sprintf("%d", unixTimestamp))
	if length >= 13 {
		// Likely a millisecond timestamp
		secs := unixTimestamp / 1000
		nsecs := (unixTimestamp % 1000) * 1e6
		return time.Unix(int64(secs), int64(nsecs))
	} else {
		// Likely a second timestamp
		return time.Unix(int64(unixTimestamp), 0)
	}
}

// ToClientReservations converts a slice of server
// reservations to a slice of client reservations.
func ToClientReservations(
	res []*reservation.Reservation) []*looprpc.ClientReservation {

	var result []*looprpc.ClientReservation
	for _, r := range res {
		result = append(result, toClientReservation(r))
	}

	return result
}

// toClientReservation converts a server reservation to a
// client reservation.
func toClientReservation(
	res *reservation.Reservation) *looprpc.ClientReservation {

	var (
		txid string
		vout uint32
	)
	if res.Outpoint != nil {
		txid = res.Outpoint.Hash.String()
		vout = res.Outpoint.Index
	}

	return &looprpc.ClientReservation{
		ReservationId: res.ID[:],
		State:         string(res.State),
		Amount:        uint64(res.Value),
		TxId:          txid,
		Vout:          vout,
		Expiry:        res.Expiry,
	}
}

// marshalFixedpoint marshals a fixed point from the tap rfqmath package to the
// looprpc package.
func marshalFixedPoint(bigIntFixedPoint *rfqmath.BigIntFixedPoint,
) *looprpc.FixedPoint {

	return &looprpc.FixedPoint{
		Coefficient: bigIntFixedPoint.Coefficient.String(),
		Scale:       uint32(bigIntFixedPoint.Scale),
	}
}
