swaps_service.go raw

   1  package swaps
   2  
   3  import (
   4  	"context"
   5  	"crypto/rand"
   6  	"crypto/sha256"
   7  	"encoding/hex"
   8  	"encoding/json"
   9  	"errors"
  10  	"fmt"
  11  	"io"
  12  	"math"
  13  	"net/http"
  14  	"strconv"
  15  	"sync"
  16  	"time"
  17  
  18  	"github.com/BoltzExchange/boltz-client/v2/pkg/boltz"
  19  	"github.com/btcsuite/btcd/btcec/v2"
  20  	"github.com/btcsuite/btcd/btcutil"
  21  	"github.com/btcsuite/btcd/btcutil/hdkeychain"
  22  	"github.com/btcsuite/btcd/chaincfg"
  23  	"github.com/getAlby/hub/config"
  24  	"github.com/getAlby/hub/constants"
  25  	"github.com/getAlby/hub/db"
  26  	"github.com/getAlby/hub/events"
  27  	"github.com/getAlby/hub/lnclient"
  28  	"github.com/getAlby/hub/logger"
  29  	"github.com/getAlby/hub/service/keys"
  30  	"github.com/getAlby/hub/transactions"
  31  	decodepay "github.com/nbd-wtf/ln-decodepay"
  32  	"github.com/sirupsen/logrus"
  33  	"gorm.io/datatypes"
  34  	"gorm.io/gorm"
  35  )
  36  
  37  type Swap = db.Swap
  38  
  39  type swapsService struct {
  40  	autoSwapOutCancelFn      context.CancelFunc
  41  	db                       *gorm.DB
  42  	ctx                      context.Context
  43  	lnClient                 lnclient.LNClient
  44  	cfg                      config.Config
  45  	keys                     keys.Keys
  46  	eventPublisher           events.EventPublisher
  47  	transactionsService      transactions.TransactionsService
  48  	boltzApi                 *boltz.Api
  49  	boltzWs                  *boltz.Websocket
  50  	swapListeners            map[string]chan boltz.SwapUpdate
  51  	swapListenersLock        sync.Mutex
  52  	autoSwapOutXpubLock      sync.Mutex
  53  	autoSwapOutDecryptedXpub string
  54  }
  55  
  56  type SwapsService interface {
  57  	StopAutoSwapOut()
  58  	EnableAutoSwapOut(encryptionKey string) error
  59  	SwapOut(amountSat uint64, destination string, autoSwap, usedXpubDerivation bool) (*SwapResponse, error)
  60  	SwapIn(amountSat uint64, autoSwap bool) (*SwapResponse, error)
  61  	GetSwapOutInfo() (*SwapInfo, error)
  62  	GetSwapInInfo() (*SwapInfo, error)
  63  	RefundSwap(swapId, address string, enableRetries bool) error
  64  	GetSwap(swapId string) (*Swap, error)
  65  	ListSwaps() ([]Swap, error)
  66  	GetDecryptedAutoSwapXpub() string
  67  	ValidateAddress(address string) error
  68  	ValidateXpub(xpub string) error
  69  }
  70  
  71  const (
  72  	AlbySwapServiceFeePercentage = 1.0
  73  )
  74  
  75  type SwapInfo struct {
  76  	AlbyServiceFee     float64
  77  	BoltzServiceFee    float64
  78  	BoltzNetworkFeeSat uint64
  79  	MinAmountSat       uint64
  80  	MaxAmountSat       uint64
  81  }
  82  
  83  type SwapResponse struct {
  84  	SwapId      string `json:"swapId"`
  85  	PaymentHash string `json:"paymentHash"`
  86  }
  87  
  88  func NewSwapsService(ctx context.Context, db *gorm.DB, cfg config.Config, keys keys.Keys, eventPublisher events.EventPublisher,
  89  	lnClient lnclient.LNClient, transactionsService transactions.TransactionsService, encryptionKey string) SwapsService {
  90  	boltzApi := &boltz.Api{URL: cfg.GetEnv().BoltzApi}
  91  	boltzWs := boltzApi.NewWebsocket()
  92  
  93  	svc := &swapsService{
  94  		ctx:                 ctx,
  95  		cfg:                 cfg,
  96  		db:                  db,
  97  		keys:                keys,
  98  		eventPublisher:      eventPublisher,
  99  		transactionsService: transactionsService,
 100  		lnClient:            lnClient,
 101  		boltzApi:            boltzApi,
 102  		boltzWs:             boltzWs,
 103  		swapListeners:       make(map[string]chan boltz.SwapUpdate),
 104  	}
 105  
 106  	go func() {
 107  		for {
 108  			update, ok := <-svc.boltzWs.Updates
 109  			if !ok {
 110  				logger.Logger.Error("Received error from boltz websocket")
 111  				continue
 112  			}
 113  
 114  			svc.swapListenersLock.Lock()
 115  			ch, ok := svc.swapListeners[update.Id]
 116  			svc.swapListenersLock.Unlock()
 117  			if ok {
 118  				ch <- update
 119  			} else {
 120  				logger.Logger.WithField("swap_id", update.Id).Error("Failed to receive update from boltz")
 121  			}
 122  		}
 123  	}()
 124  
 125  	err := svc.EnableAutoSwapOut(encryptionKey)
 126  	if err != nil {
 127  		logger.Logger.WithError(err).Error("Couldn't enable auto swaps")
 128  	}
 129  
 130  	go svc.subscribePendingSwaps()
 131  
 132  	return svc
 133  }
 134  
 135  func (svc *swapsService) StopAutoSwapOut() {
 136  	if svc.autoSwapOutCancelFn != nil {
 137  		logger.Logger.Info("Stopping auto swap out service...")
 138  		svc.autoSwapOutCancelFn()
 139  		logger.Logger.Info("Auto swap out service stopped")
 140  	}
 141  	svc.autoSwapOutXpubLock.Lock()
 142  	svc.autoSwapOutDecryptedXpub = ""
 143  	svc.autoSwapOutXpubLock.Unlock()
 144  }
 145  
 146  func (svc *swapsService) EnableAutoSwapOut(encryptionKey string) error {
 147  	svc.StopAutoSwapOut()
 148  
 149  	ctx, cancelFn := context.WithCancel(svc.ctx)
 150  	svc.autoSwapOutXpubLock.Lock()
 151  	svc.autoSwapOutDecryptedXpub = ""
 152  	svc.autoSwapOutXpubLock.Unlock()
 153  
 154  	swapDestination, _ := svc.cfg.Get(config.AutoSwapDestinationKey, encryptionKey)
 155  	if swapDestination != "" {
 156  		if err := svc.ValidateXpub(swapDestination); err == nil {
 157  			svc.autoSwapOutXpubLock.Lock()
 158  			svc.autoSwapOutDecryptedXpub = swapDestination
 159  			svc.autoSwapOutXpubLock.Unlock()
 160  		}
 161  	}
 162  
 163  	balanceThresholdStr, _ := svc.cfg.Get(config.AutoSwapBalanceThresholdKey, "")
 164  	amountStr, _ := svc.cfg.Get(config.AutoSwapAmountKey, "")
 165  
 166  	if balanceThresholdStr == "" || amountStr == "" {
 167  		cancelFn()
 168  		logger.Logger.Info("Auto swap not configured")
 169  		return nil
 170  	}
 171  
 172  	balanceThreshold, err := strconv.ParseUint(balanceThresholdStr, 10, 64)
 173  	if err != nil {
 174  		cancelFn()
 175  		return errors.New("invalid auto swap configuration")
 176  	}
 177  
 178  	amountSat, err := strconv.ParseUint(amountStr, 10, 64)
 179  	if err != nil {
 180  		cancelFn()
 181  		return errors.New("invalid auto swap configuration")
 182  	}
 183  
 184  	logger.Logger.Info("Starting auto swap workflow")
 185  
 186  	go func() {
 187  		for {
 188  			select {
 189  			case <-time.After(1 * time.Hour):
 190  				logger.Logger.Debug("Checking to see if we can swap")
 191  				balance, err := svc.lnClient.GetBalances(ctx, false)
 192  				if err != nil {
 193  					logger.Logger.WithError(err).Error("Failed to get balance")
 194  					continue
 195  				}
 196  				lightningBalance := uint64(balance.Lightning.TotalSpendableMsat)
 197  				balanceThresholdMilliSats := balanceThreshold * 1000
 198  				if lightningBalance < balanceThresholdMilliSats {
 199  					logger.Logger.Info("Threshold requirements not met for swap, ignoring")
 200  					continue
 201  				}
 202  
 203  				actualDestination := swapDestination
 204  				var usedXpubDerivation bool
 205  				// Check if we have a decrypted XPUB in memory
 206  				svc.autoSwapOutXpubLock.Lock()
 207  				hasDecryptedXpub := svc.autoSwapOutDecryptedXpub != ""
 208  				svc.autoSwapOutXpubLock.Unlock()
 209  				if hasDecryptedXpub {
 210  					actualDestination, err = svc.getNextUnusedAddressFromXpub()
 211  					if err != nil {
 212  						logger.Logger.WithError(err).Error("Failed to get next address from xpub")
 213  						continue
 214  					}
 215  					usedXpubDerivation = true
 216  				}
 217  
 218  				logger.Logger.WithFields(logrus.Fields{
 219  					"amountSat":   amountSat,
 220  					"destination": actualDestination,
 221  				}).Info("Initiating swap")
 222  				_, err = svc.SwapOut(amountSat, actualDestination, true, usedXpubDerivation)
 223  				if err != nil {
 224  					logger.Logger.WithError(err).Error("Failed to initiate swap")
 225  					continue
 226  				}
 227  			case <-ctx.Done():
 228  				logger.Logger.Info("Stopping auto swap workflow")
 229  				return
 230  			}
 231  		}
 232  	}()
 233  
 234  	svc.autoSwapOutCancelFn = cancelFn
 235  
 236  	return nil
 237  }
 238  
 239  func (svc *swapsService) SwapOut(amountSat uint64, destination string, autoSwap, usedXpubDerivation bool) (*SwapResponse, error) {
 240  	if destination == "" {
 241  		var err error
 242  		destination, err = svc.lnClient.GetNewOnchainAddress(svc.ctx)
 243  		if err != nil {
 244  			return nil, fmt.Errorf("could not get onchain address from config: %s", err)
 245  		}
 246  	}
 247  
 248  	preimage := make([]byte, 32)
 249  	_, err := rand.Read(preimage)
 250  	if err != nil {
 251  		return nil, err
 252  	}
 253  	preimageHash := sha256.Sum256(preimage)
 254  	paymentHash := hex.EncodeToString(preimageHash[:])
 255  
 256  	reversePairs, err := svc.boltzApi.GetReversePairs()
 257  	if err != nil {
 258  		return nil, fmt.Errorf("could not get reverse pairs: %s", err)
 259  	}
 260  
 261  	pair := boltz.Pair{From: boltz.CurrencyBtc, To: boltz.CurrencyBtc}
 262  	pairInfo, err := boltz.FindPair(pair, reversePairs)
 263  	if err != nil {
 264  		return nil, fmt.Errorf("could not find reverse pair: %s", err)
 265  	}
 266  
 267  	fees := pairInfo.Fees
 268  	serviceFeePercentage := boltz.Percentage(fees.Percentage)
 269  
 270  	serviceFeeSat := boltz.CalculatePercentage(serviceFeePercentage, amountSat)
 271  	networkFeeSat := fees.MinerFees.Lockup + fees.MinerFees.Claim
 272  
 273  	logger.Logger.WithFields(logrus.Fields{
 274  		"serviceFeeSat": serviceFeeSat,
 275  		"networkFeeSat": networkFeeSat,
 276  	}).Info("Calculated fees for swap out")
 277  
 278  	albyFee := &boltz.ExtraFees{
 279  		Percentage: AlbySwapServiceFeePercentage,
 280  		Id:         "albyServiceFee",
 281  	}
 282  
 283  	dbSwap := db.Swap{
 284  		Type:               constants.SWAP_TYPE_OUT,
 285  		State:              constants.SWAP_STATE_PENDING,
 286  		DestinationAddress: destination,
 287  		PaymentHash:        paymentHash,
 288  		Preimage:           hex.EncodeToString(preimage),
 289  		AutoSwap:           autoSwap,
 290  		UsedXpub:           usedXpubDerivation,
 291  		ReceiveAmountSat:   amountSat,
 292  	}
 293  
 294  	var ourKeys *btcec.PrivateKey
 295  	var swap *boltz.CreateReverseSwapResponse
 296  
 297  	defer func() {
 298  		if err != nil && dbSwap.ID != 0 {
 299  			logger.Logger.WithError(err).Error("Marking swap state as failed")
 300  			svc.markSwapState(&dbSwap, constants.SWAP_STATE_FAILED)
 301  		}
 302  	}()
 303  
 304  	err = svc.db.Transaction(func(tx *gorm.DB) error {
 305  		err := tx.Save(&dbSwap).Error
 306  		if err != nil {
 307  			return err
 308  		}
 309  
 310  		ourKeys, err = svc.keys.GetSwapKey(dbSwap.ID)
 311  		if err != nil {
 312  			return fmt.Errorf("error generating swap child private key: %w", err)
 313  		}
 314  
 315  		swapRequest := boltz.CreateReverseSwapRequest{
 316  			From:           boltz.CurrencyBtc,
 317  			To:             boltz.CurrencyBtc,
 318  			ClaimPublicKey: ourKeys.PubKey().SerializeCompressed(),
 319  			PreimageHash:   preimageHash[:],
 320  			Description:    "Lightning to on-chain swap",
 321  			PairHash:       pairInfo.Hash,
 322  			ReferralId:     "alby",
 323  			ExtraFees:      albyFee,
 324  			OnchainAmount:  amountSat + fees.MinerFees.Claim,
 325  		}
 326  
 327  		swap, err = svc.boltzApi.CreateReverseSwap(swapRequest)
 328  
 329  		if err != nil {
 330  			return fmt.Errorf("could not create swap: %s", err)
 331  		}
 332  
 333  		swapTreeJson, err := json.Marshal(swap.SwapTree)
 334  		if err != nil {
 335  			return err
 336  		}
 337  
 338  		maxSendAmountSat := calculateMaxSwapOutSendAmountSat(amountSat, fees.Percentage, fees.MinerFees.Lockup, fees.MinerFees.Claim)
 339  		sendAmountSat, err := verifySwapOutInvoice(swap.Invoice, paymentHash, maxSendAmountSat)
 340  		if err != nil {
 341  			return fmt.Errorf("invalid swap invoice: %w", err)
 342  		}
 343  
 344  		err = tx.Model(&dbSwap).Updates(&db.Swap{
 345  			SwapId:             swap.Id,
 346  			SendAmountSat:      sendAmountSat,
 347  			Invoice:            swap.Invoice,
 348  			LockupAddress:      swap.LockupAddress,
 349  			TimeoutBlockHeight: swap.TimeoutBlockHeight,
 350  			BoltzPubkey:        hex.EncodeToString(swap.RefundPublicKey),
 351  			SwapTree:           datatypes.JSON(swapTreeJson),
 352  		}).Error
 353  		if err != nil {
 354  			return err
 355  		}
 356  
 357  		// commit transaction
 358  		return nil
 359  	})
 360  
 361  	if err != nil {
 362  		logger.Logger.WithError(err).WithFields(logrus.Fields{
 363  			"paymentHash": paymentHash,
 364  		}).Error("Failed to save swap")
 365  		return nil, err
 366  	}
 367  
 368  	logger.Logger.WithField("swapId", swap.Id).Info("Swap created")
 369  
 370  	if autoSwap {
 371  		// block until the swap finishes to ensure we can't do multiple concurrent auto swaps
 372  		svc.startSwapOutListener(&dbSwap)
 373  	} else {
 374  		// run in parallel as we need to return the swap ID in the HTTP response
 375  		go svc.startSwapOutListener(&dbSwap)
 376  	}
 377  
 378  	return &SwapResponse{
 379  		SwapId:      swap.Id,
 380  		PaymentHash: paymentHash,
 381  	}, nil
 382  }
 383  
 384  // swapOutInvoiceToleranceSat covers rounding differences that can occur when
 385  // the swap provider converts the requested on-chain amount into an invoice amount.
 386  const swapOutInvoiceToleranceSat = 10
 387  
 388  // calculateMaxSwapOutSendAmountSat returns the maximum invoice amount accepted for
 389  // a swap out: the requested on-chain amount plus the quoted miner fees, marked up
 390  // by the quoted percentage fees (which are charged on the invoice amount), plus a
 391  // small rounding tolerance.
 392  func calculateMaxSwapOutSendAmountSat(receiveAmountSat uint64, serviceFeePercentage float64, lockupFeeSat uint64, claimFeeSat uint64) uint64 {
 393  	totalFeePercentage := serviceFeePercentage + AlbySwapServiceFeePercentage
 394  	if totalFeePercentage >= 100 {
 395  		return 0
 396  	}
 397  	onchainAmountSat := float64(receiveAmountSat + claimFeeSat + lockupFeeSat)
 398  	expectedSendAmountSat := math.Ceil(onchainAmountSat / (1 - totalFeePercentage/100))
 399  	return uint64(expectedSendAmountSat) + swapOutInvoiceToleranceSat
 400  }
 401  
 402  // verifySwapOutInvoice checks that a swap out invoice is bound to the swap's
 403  // payment hash and does not exceed maxSendAmountSat, and returns its amount.
 404  func verifySwapOutInvoice(invoice string, expectedPaymentHash string, maxSendAmountSat uint64) (uint64, error) {
 405  	paymentRequest, err := decodepay.Decodepay(invoice)
 406  	if err != nil {
 407  		return 0, fmt.Errorf("failed to decode bolt11 invoice: %w", err)
 408  	}
 409  	if paymentRequest.PaymentHash != expectedPaymentHash {
 410  		return 0, fmt.Errorf("invoice payment hash %s does not match swap payment hash %s", paymentRequest.PaymentHash, expectedPaymentHash)
 411  	}
 412  	if paymentRequest.MSatoshi <= 0 {
 413  		return 0, errors.New("invoice does not have an amount")
 414  	}
 415  	sendAmountSat := uint64(paymentRequest.MSatoshi) / 1000
 416  	if sendAmountSat > maxSendAmountSat {
 417  		return 0, fmt.Errorf("invoice amount %d sat exceeds maximum expected amount %d sat", sendAmountSat, maxSendAmountSat)
 418  	}
 419  	return sendAmountSat, nil
 420  }
 421  
 422  func (svc *swapsService) SwapIn(amountSat uint64, autoSwap bool) (*SwapResponse, error) {
 423  	amountMsat := amountSat * 1000
 424  	invoice, err := svc.transactionsService.MakeInvoice(svc.ctx, amountMsat, "On-chain to lightning swap", "", 0, nil, svc.lnClient, nil, nil, nil)
 425  	if err != nil {
 426  		return nil, err
 427  	}
 428  
 429  	submarinePairs, err := svc.boltzApi.GetSubmarinePairs()
 430  	if err != nil {
 431  		return nil, fmt.Errorf("could not get submarine pairs: %s", err)
 432  	}
 433  
 434  	pair := boltz.Pair{From: boltz.CurrencyBtc, To: boltz.CurrencyBtc}
 435  	pairInfo, err := boltz.FindPair(pair, submarinePairs)
 436  	if err != nil {
 437  		return nil, fmt.Errorf("could not find submarine pair: %s", err)
 438  	}
 439  
 440  	fees := pairInfo.Fees
 441  	serviceFeePercentage := boltz.Percentage(fees.Percentage)
 442  
 443  	serviceFeeSat := boltz.CalculatePercentage(serviceFeePercentage, amountSat)
 444  	networkFeeSat := fees.MinerFees
 445  
 446  	logger.Logger.WithFields(logrus.Fields{
 447  		"serviceFeeSat": serviceFeeSat,
 448  		"networkFeeSat": networkFeeSat,
 449  	}).Info("Calculated fees for swap in")
 450  
 451  	albyFee := &boltz.ExtraFees{
 452  		Percentage: AlbySwapServiceFeePercentage,
 453  		Id:         "albyServiceFee",
 454  	}
 455  
 456  	dbSwap := db.Swap{
 457  		Type:        constants.SWAP_TYPE_IN,
 458  		State:       constants.SWAP_STATE_PENDING,
 459  		Invoice:     invoice.PaymentRequest,
 460  		PaymentHash: invoice.PaymentHash,
 461  		AutoSwap:    autoSwap,
 462  	}
 463  
 464  	var ourKeys *btcec.PrivateKey
 465  	var swap *boltz.CreateSwapResponse
 466  
 467  	defer func() {
 468  		if err != nil && dbSwap.ID != 0 {
 469  			logger.Logger.WithError(err).Error("Marking swap state as failed")
 470  			svc.markSwapState(&dbSwap, constants.SWAP_STATE_FAILED)
 471  		}
 472  	}()
 473  
 474  	err = svc.db.Transaction(func(tx *gorm.DB) error {
 475  		err := tx.Save(&dbSwap).Error
 476  		if err != nil {
 477  			return err
 478  		}
 479  
 480  		ourKeys, err = svc.keys.GetSwapKey(dbSwap.ID)
 481  		if err != nil {
 482  			return fmt.Errorf("error generating swap child private key: %w", err)
 483  		}
 484  
 485  		swap, err = svc.boltzApi.CreateSwap(boltz.CreateSwapRequest{
 486  			From:            boltz.CurrencyBtc,
 487  			To:              boltz.CurrencyBtc,
 488  			RefundPublicKey: ourKeys.PubKey().SerializeCompressed(),
 489  			Invoice:         invoice.PaymentRequest,
 490  			PairHash:        pairInfo.Hash,
 491  			ReferralId:      "alby",
 492  			ExtraFees:       albyFee,
 493  		})
 494  		if err != nil {
 495  			return fmt.Errorf("could not create swap: %s", err)
 496  		}
 497  
 498  		swapTreeJson, err := json.Marshal(swap.SwapTree)
 499  		if err != nil {
 500  			return err
 501  		}
 502  
 503  		err = tx.Model(&dbSwap).Updates(&db.Swap{
 504  			SwapId:             swap.Id,
 505  			SendAmountSat:      swap.ExpectedAmount,
 506  			LockupAddress:      swap.Address,
 507  			TimeoutBlockHeight: swap.TimeoutBlockHeight,
 508  			BoltzPubkey:        hex.EncodeToString(swap.ClaimPublicKey),
 509  			SwapTree:           datatypes.JSON(swapTreeJson),
 510  		}).Error
 511  		if err != nil {
 512  			return err
 513  		}
 514  
 515  		// commit transaction
 516  		return nil
 517  	})
 518  
 519  	if err != nil {
 520  		logger.Logger.WithError(err).WithFields(logrus.Fields{
 521  			"paymentHash": invoice.PaymentHash,
 522  		}).Error("Failed to save swap")
 523  		return nil, err
 524  	}
 525  
 526  	metadata := map[string]interface{}{
 527  		"swap_id": swap.Id,
 528  	}
 529  	err = svc.transactionsService.SetTransactionMetadata(svc.ctx, invoice.ID, metadata)
 530  	if err != nil {
 531  		logger.Logger.WithError(err).WithFields(logrus.Fields{
 532  			"swapId":      swap.Id,
 533  			"paymentHash": invoice.PaymentHash,
 534  			"metadata":    metadata,
 535  		}).Error("Failed to add swap metadata to lightning payment")
 536  		return nil, err
 537  	}
 538  
 539  	logger.Logger.WithField("swapId", swap.Id).Info("Swap created")
 540  
 541  	go svc.startSwapInListener(&dbSwap)
 542  
 543  	return &SwapResponse{
 544  		SwapId:      swap.Id,
 545  		PaymentHash: invoice.PaymentHash,
 546  	}, nil
 547  }
 548  
 549  func (svc *swapsService) GetSwapOutInfo() (*SwapInfo, error) {
 550  	reversePairs, err := svc.boltzApi.GetReversePairs()
 551  	if err != nil {
 552  		return nil, fmt.Errorf("could not get reverse pairs: %s", err)
 553  	}
 554  
 555  	pair := boltz.Pair{From: boltz.CurrencyBtc, To: boltz.CurrencyBtc}
 556  	pairInfo, err := boltz.FindPair(pair, reversePairs)
 557  	if err != nil {
 558  		return nil, fmt.Errorf("could not find reverse pair: %s", err)
 559  	}
 560  
 561  	fees := pairInfo.Fees
 562  	limits := pairInfo.Limits
 563  
 564  	return &SwapInfo{
 565  		AlbyServiceFee:     AlbySwapServiceFeePercentage,
 566  		BoltzServiceFee:    fees.Percentage,
 567  		BoltzNetworkFeeSat: fees.MinerFees.Lockup + fees.MinerFees.Claim,
 568  		MinAmountSat:       limits.Minimal,
 569  		MaxAmountSat:       limits.Maximal,
 570  	}, nil
 571  }
 572  
 573  func (svc *swapsService) GetSwapInInfo() (*SwapInfo, error) {
 574  	submarinePairs, err := svc.boltzApi.GetSubmarinePairs()
 575  	if err != nil {
 576  		return nil, fmt.Errorf("could not get reverse pairs: %s", err)
 577  	}
 578  
 579  	pair := boltz.Pair{From: boltz.CurrencyBtc, To: boltz.CurrencyBtc}
 580  	pairInfo, err := boltz.FindPair(pair, submarinePairs)
 581  	if err != nil {
 582  		return nil, fmt.Errorf("could not find reverse pair: %s", err)
 583  	}
 584  
 585  	fees := pairInfo.Fees
 586  	limits := pairInfo.Limits
 587  
 588  	return &SwapInfo{
 589  		AlbyServiceFee:     AlbySwapServiceFeePercentage,
 590  		BoltzServiceFee:    fees.Percentage,
 591  		BoltzNetworkFeeSat: fees.MinerFees,
 592  		MinAmountSat:       limits.Minimal,
 593  		MaxAmountSat:       limits.Maximal,
 594  	}, nil
 595  }
 596  
 597  func (svc *swapsService) markSwapState(dbSwap *db.Swap, state string) {
 598  	if svc.db.Limit(1).Find(dbSwap, &db.Swap{
 599  		SwapId: dbSwap.SwapId,
 600  		State:  state,
 601  	}).RowsAffected > 0 {
 602  		logger.Logger.WithField("swapId", dbSwap.SwapId).Debugf("swap already marked as %s", state)
 603  		return
 604  	}
 605  
 606  	dbErr := svc.db.Model(dbSwap).Updates(&db.Swap{
 607  		State: state,
 608  	}).Error
 609  	if dbErr != nil {
 610  		logger.Logger.WithError(dbErr).WithField("swapId", dbSwap.SwapId).Error("Failed to update swap state")
 611  	}
 612  }
 613  
 614  func (svc *swapsService) RefundSwap(swapId, address string, enableRetries bool) error {
 615  	var swap db.Swap
 616  	query := svc.db.Limit(1).Find(&swap, &db.Swap{
 617  		SwapId: swapId,
 618  	})
 619  	err := query.Error
 620  	if err != nil {
 621  		logger.Logger.WithField("swapId", swapId).WithError(err).Error("Failed to lookup swap")
 622  		return err
 623  	}
 624  	if query.RowsAffected == 0 {
 625  		logger.Logger.WithField("swapId", swapId).Error("Could not find swap to process refund")
 626  		return errors.New("Could not find swap")
 627  	}
 628  
 629  	if swap.Type != constants.SWAP_TYPE_IN {
 630  		return errors.New("only On-chain -> Lightning swaps can be refunded")
 631  	}
 632  
 633  	if swap.ClaimTxId != "" {
 634  		return fmt.Errorf("refund already processed with claim txid: %s", swap.ClaimTxId)
 635  	}
 636  
 637  	network, err := boltz.ParseChain(svc.cfg.GetNetwork())
 638  	if err != nil {
 639  		return err
 640  	}
 641  
 642  	// Fetch raw hex to construct the lockup transaction
 643  	swapTransactionResp, err := svc.boltzApi.GetSwapTransaction(swapId)
 644  	if err != nil {
 645  		logger.Logger.WithField("swapId", swapId).WithError(err).Error("Failed to get lockup tx from swap id")
 646  		return err
 647  	}
 648  
 649  	if swap.LockupTxId == "" {
 650  		err = svc.db.Model(&swap).Updates(&db.Swap{
 651  			LockupTxId: swapTransactionResp.Id,
 652  		}).Error
 653  		if err != nil {
 654  			logger.Logger.WithFields(logrus.Fields{
 655  				"swapId":     swapId,
 656  				"lockupTxId": swapTransactionResp.Id,
 657  			}).WithError(err).Error("Failed to save lockup txid to swap")
 658  			return err
 659  		}
 660  	}
 661  
 662  	ourKeys, err := svc.keys.GetSwapKey(swap.ID)
 663  	if err != nil {
 664  		return fmt.Errorf("error generating swap child private key: %w", err)
 665  	}
 666  
 667  	var serializedTree boltz.SerializedTree
 668  	if err := json.Unmarshal(swap.SwapTree, &serializedTree); err != nil {
 669  		return err
 670  	}
 671  
 672  	boltzPubkeyBytes, err := hex.DecodeString(swap.BoltzPubkey)
 673  	if err != nil {
 674  		return fmt.Errorf("invalid boltz pubkey: %v", err)
 675  	}
 676  
 677  	boltzPubKey, err := btcec.ParsePubKey(boltzPubkeyBytes)
 678  	if err != nil {
 679  		return err
 680  	}
 681  
 682  	decodedPreimageHash, err := hex.DecodeString(swap.PaymentHash)
 683  	if err != nil {
 684  		return fmt.Errorf("invalid preimage hash: %v", err)
 685  	}
 686  
 687  	tree := serializedTree.Deserialize()
 688  	if err := tree.Init(boltz.CurrencyBtc, false, ourKeys, boltzPubKey); err != nil {
 689  		return err
 690  	}
 691  
 692  	if err := tree.Check(boltz.NormalSwap, swap.TimeoutBlockHeight, decodedPreimageHash); err != nil {
 693  		return err
 694  	}
 695  
 696  	if err := tree.CheckAddress(swap.LockupAddress, network, nil); err != nil {
 697  		return err
 698  	}
 699  
 700  	lockupTransaction, err := boltz.NewTxFromHex(boltz.CurrencyBtc, swapTransactionResp.Hex, nil)
 701  	if err != nil {
 702  		logger.Logger.WithField("swapId", swapId).WithError(err).Error("Failed to build lockup tx from hex")
 703  		return err
 704  	}
 705  	vout, _, err := lockupTransaction.FindVout(network, swap.LockupAddress)
 706  	if err != nil {
 707  		logger.Logger.WithField("swapId", swapId).WithError(err).Error("Failed to find lockup address output")
 708  		return err
 709  	}
 710  
 711  	if address == "" {
 712  		address, err = svc.lnClient.GetNewOnchainAddress(svc.ctx)
 713  		if err != nil {
 714  			logger.Logger.WithField("swapId", swapId).WithError(err).Error("Failed to get new on-chain address from config")
 715  			return err
 716  		}
 717  	}
 718  
 719  	err = svc.db.Model(&swap).Updates(&db.Swap{
 720  		RefundAddress: address,
 721  	}).Error
 722  	if err != nil {
 723  		logger.Logger.WithFields(logrus.Fields{
 724  			"swapId":        swapId,
 725  			"refundAddress": address,
 726  		}).WithError(err).Error("Failed to save refund address to swap")
 727  		return err
 728  	}
 729  
 730  	var refundTransaction boltz.Transaction
 731  
 732  	for i := 0; ; i++ {
 733  		select {
 734  		case <-svc.ctx.Done():
 735  			logger.Logger.WithField("swapId", swapId).Info("Swap refund context cancelled")
 736  			return nil
 737  		case <-time.After(time.Duration(min(i*5, 30)) * time.Second): // timeout
 738  		}
 739  
 740  		nodeInfo, err := svc.lnClient.GetInfo(svc.ctx)
 741  		if err != nil {
 742  			logger.Logger.WithError(err).WithFields(logrus.Fields{
 743  				"swapId":    swapId,
 744  				"iteration": i,
 745  			}).WithError(err).Error("Failed to request node info")
 746  			continue
 747  		}
 748  
 749  		feeRate, err := svc.getFeeRate()
 750  		if err != nil {
 751  			logger.Logger.WithError(err).WithFields(logrus.Fields{
 752  				"swapId":    swapId,
 753  				"iteration": i,
 754  			}).Error("Failed to fetch fee rate to create claim transaction")
 755  			continue
 756  		}
 757  
 758  		cooperative := swapTransactionResp.TimeoutBlockHeight > nodeInfo.BlockHeight
 759  
 760  		refundTransaction, _, err = boltz.ConstructTransaction(
 761  			network,
 762  			boltz.CurrencyBtc,
 763  			[]boltz.OutputDetails{
 764  				{
 765  					SwapId:             swapId,
 766  					SwapType:           boltz.NormalSwap,
 767  					Address:            address,
 768  					LockupTransaction:  lockupTransaction,
 769  					TimeoutBlockHeight: swapTransactionResp.TimeoutBlockHeight,
 770  					Vout:               vout,
 771  					PrivateKey:         ourKeys,
 772  					SwapTree:           tree,
 773  					Cooperative:        cooperative,
 774  				},
 775  			},
 776  			boltz.Fee{
 777  				SatsPerVbyte: &feeRate,
 778  			},
 779  			svc.boltzApi,
 780  		)
 781  		if err != nil {
 782  			logger.Logger.WithFields(logrus.Fields{
 783  				"swapId":      swapId,
 784  				"iteration":   i,
 785  				"cooperative": cooperative,
 786  			}).WithError(err).Error("Could not create claim transaction refund")
 787  			if enableRetries && cooperative {
 788  				continue
 789  			}
 790  			return err
 791  		}
 792  		break
 793  	}
 794  
 795  	vout, _, _ = refundTransaction.FindVout(network, address)
 796  	refundAmountSat, _ := refundTransaction.VoutValue(vout)
 797  
 798  	txHex, err := refundTransaction.Serialize()
 799  	if err != nil {
 800  		logger.Logger.WithField("swapId", swapId).WithError(err).Error("Could not serialize refund transaction")
 801  		return err
 802  	}
 803  
 804  	// TODO: Replace with LNClient broadcast method to avoid trusting boltz
 805  	claimTxId, err := svc.boltzApi.BroadcastTransaction(boltz.CurrencyBtc, txHex)
 806  	if err != nil {
 807  		logger.Logger.WithField("swapId", swapId).WithError(err).Error("Could not broadcast transaction")
 808  		return err
 809  	}
 810  
 811  	logger.Logger.WithFields(logrus.Fields{
 812  		"swapId":    swapId,
 813  		"claimTxId": claimTxId,
 814  	}).Info("Claim transaction broadcasted for refund")
 815  
 816  	err = svc.db.Model(&swap).Updates(&db.Swap{
 817  		ClaimTxId:        claimTxId,
 818  		ReceiveAmountSat: refundAmountSat,
 819  		State:            constants.SWAP_STATE_REFUNDED,
 820  	}).Error
 821  	if err != nil {
 822  		logger.Logger.WithFields(logrus.Fields{
 823  			"swapId":    swapId,
 824  			"claimTxId": claimTxId,
 825  		}).WithError(err).Error("Failed to save claim txid to swap")
 826  		return err
 827  	}
 828  
 829  	return nil
 830  }
 831  
 832  func (svc *swapsService) GetSwap(swapId string) (*Swap, error) {
 833  	var swap db.Swap
 834  	err := svc.db.Limit(1).Find(&swap, &db.Swap{
 835  		SwapId: swapId,
 836  	}).Error
 837  
 838  	if err != nil {
 839  		logger.Logger.WithError(err).Error("Failed to get swap")
 840  		return nil, err
 841  	}
 842  
 843  	return &swap, nil
 844  }
 845  
 846  func (svc *swapsService) ListSwaps() ([]Swap, error) {
 847  	var swaps []db.Swap
 848  	err := svc.db.Find(&swaps).Error
 849  
 850  	if err != nil {
 851  		logger.Logger.WithError(err).Error("Failed to list swaps")
 852  		return nil, err
 853  	}
 854  
 855  	return swaps, nil
 856  }
 857  
 858  func (svc *swapsService) subscribePendingSwaps() {
 859  	var swaps []db.Swap
 860  	if err := svc.db.Where("state = ?", constants.SWAP_STATE_PENDING).Find(&swaps).Error; err != nil {
 861  		logger.Logger.WithError(err).Error("failed to load pending swaps")
 862  		return
 863  	}
 864  	if len(swaps) == 0 {
 865  		return
 866  	}
 867  
 868  	logger.Logger.WithField("count", len(swaps)).Info("Resuming pending swaps...")
 869  
 870  	ids := make([]string, len(swaps))
 871  	for i, s := range swaps {
 872  		ids[i] = s.SwapId
 873  	}
 874  
 875  	for _, swap := range swaps {
 876  		switch swap.Type {
 877  		case constants.SWAP_TYPE_IN:
 878  			go svc.startSwapInListener(&swap)
 879  		case constants.SWAP_TYPE_OUT:
 880  			go svc.startSwapOutListener(&swap)
 881  		}
 882  	}
 883  }
 884  
 885  func (svc *swapsService) startSwapInListener(swap *db.Swap) {
 886  	updateCh := make(chan boltz.SwapUpdate, 1)
 887  	svc.swapListenersLock.Lock()
 888  	svc.swapListeners[swap.SwapId] = updateCh
 889  	svc.swapListenersLock.Unlock()
 890  
 891  	for {
 892  		err := svc.boltzWs.Subscribe([]string{swap.SwapId})
 893  		if err != nil {
 894  			logger.Logger.WithError(err).Error("Failed to subscribe to boltz websocket, retrying in 2s...")
 895  			time.Sleep(2 * time.Second)
 896  			continue
 897  		}
 898  		break
 899  	}
 900  
 901  	logger.Logger.WithField("swapId", swap.SwapId).Info("Subscribed to boltz websocket")
 902  
 903  	var err error
 904  	defer func() {
 905  		svc.swapListenersLock.Lock()
 906  		delete(svc.swapListeners, swap.SwapId)
 907  		svc.swapListenersLock.Unlock()
 908  		svc.boltzWs.Unsubscribe(swap.SwapId)
 909  		if err != nil {
 910  			logger.Logger.WithError(err).Error("Marking swap state as failed")
 911  			svc.markSwapState(swap, constants.SWAP_STATE_FAILED)
 912  		}
 913  	}()
 914  
 915  	var network *boltz.Network
 916  	network, err = boltz.ParseChain(svc.cfg.GetNetwork())
 917  	if err != nil {
 918  		logger.Logger.WithError(err).WithFields(logrus.Fields{
 919  			"swapId": swap.SwapId,
 920  		}).Error("Failed to parse network")
 921  		return
 922  	}
 923  
 924  	var ourKeys *btcec.PrivateKey
 925  	ourKeys, err = svc.keys.GetSwapKey(swap.ID)
 926  	if err != nil {
 927  		logger.Logger.WithError(err).WithFields(logrus.Fields{
 928  			"swapId": swap.SwapId,
 929  		}).Error("Failed to generate swap child private key")
 930  		return
 931  	}
 932  
 933  	var serializedTree boltz.SerializedTree
 934  	if err = json.Unmarshal(swap.SwapTree, &serializedTree); err != nil {
 935  		logger.Logger.WithError(err).WithFields(logrus.Fields{
 936  			"swapId": swap.SwapId,
 937  		}).Error("Failed to unmarshal swap tree")
 938  		return
 939  	}
 940  
 941  	boltzPubkeyBytes, _ := hex.DecodeString(swap.BoltzPubkey)
 942  
 943  	var boltzPubKey *btcec.PublicKey
 944  	boltzPubKey, err = btcec.ParsePubKey(boltzPubkeyBytes)
 945  	if err != nil {
 946  		logger.Logger.WithError(err).WithFields(logrus.Fields{
 947  			"swapId": swap.SwapId,
 948  		}).Error("Failed to parse boltz pubkey")
 949  		return
 950  	}
 951  
 952  	decodedPreimageHash, _ := hex.DecodeString(swap.PaymentHash)
 953  
 954  	tree := serializedTree.Deserialize()
 955  	if err = tree.Init(boltz.CurrencyBtc, false, ourKeys, boltzPubKey); err != nil {
 956  		logger.Logger.WithError(err).WithFields(logrus.Fields{
 957  			"swapId": swap.SwapId,
 958  		}).Error("Failed to initialize swap tree")
 959  		return
 960  	}
 961  
 962  	if err = tree.Check(boltz.NormalSwap, swap.TimeoutBlockHeight, decodedPreimageHash); err != nil {
 963  		logger.Logger.WithError(err).WithFields(logrus.Fields{
 964  			"swapId": swap.SwapId,
 965  		}).Error("Failed to check swap tree")
 966  		return
 967  	}
 968  
 969  	if err = tree.CheckAddress(swap.LockupAddress, network, nil); err != nil {
 970  		logger.Logger.WithError(err).WithFields(logrus.Fields{
 971  			"swapId": swap.SwapId,
 972  		}).Error("Failed to check address")
 973  		return
 974  	}
 975  
 976  	paymentRequest, _ := decodepay.Decodepay(swap.Invoice)
 977  	amount := uint64(paymentRequest.MSatoshi / 1000)
 978  
 979  	for {
 980  		select {
 981  		case <-svc.ctx.Done():
 982  			logger.Logger.WithError(svc.ctx.Err()).WithFields(logrus.Fields{
 983  				"swapId": swap.SwapId,
 984  			}).Error("Swap in context cancelled")
 985  			return
 986  		case update, ok := <-updateCh:
 987  			if !ok {
 988  				logger.Logger.WithField("swap_id", update.Id).Error("Failed to receive update from boltz")
 989  				continue
 990  			}
 991  			if update.Id != swap.SwapId {
 992  				continue
 993  			}
 994  			switch boltz.ParseEvent(update.Status) {
 995  			case boltz.TransactionMempool:
 996  				logger.Logger.WithFields(logrus.Fields{
 997  					"swapId":     swap.SwapId,
 998  					"lockupTxId": update.Transaction.Id,
 999  				}).Info("Lockup transaction found in mempool")
1000  				err = svc.db.Model(swap).Updates(&db.Swap{
1001  					LockupTxId: update.Transaction.Id,
1002  				}).Error
1003  				if err != nil {
1004  					logger.Logger.WithFields(logrus.Fields{
1005  						"swapId":     swap.SwapId,
1006  						"lockupTxId": update.Transaction.Id,
1007  					}).WithError(err).Error("Failed to save lockup txid to swap")
1008  					return
1009  				}
1010  			case boltz.TransactionConfirmed:
1011  				logger.Logger.WithFields(logrus.Fields{
1012  					"swapId":     swap.SwapId,
1013  					"lockupTxId": swap.LockupTxId,
1014  				}).Info("Lockup transaction confirmed in mempool")
1015  			case boltz.InvoicePaid:
1016  				svc.markSwapState(swap, constants.SWAP_STATE_SUCCESS)
1017  				err = svc.db.Model(swap).Updates(&db.Swap{
1018  					ReceiveAmountSat: amount,
1019  				}).Error
1020  				if err != nil {
1021  					logger.Logger.WithFields(logrus.Fields{
1022  						"swapId":        swap.SwapId,
1023  						"receiveAmount": amount,
1024  					}).WithError(err).Error("Failed to save received amount to swap")
1025  					return
1026  				}
1027  				logger.Logger.WithField("swapId", swap.SwapId).Info("Swap succeeded")
1028  				svc.eventPublisher.Publish(&events.Event{
1029  					Event: "nwc_swap_succeeded",
1030  					Properties: map[string]interface{}{
1031  						"swapType": constants.SWAP_TYPE_IN,
1032  					},
1033  				})
1034  				return
1035  			case boltz.TransactionLockupFailed, boltz.InvoiceFailedToPay, boltz.SwapExpired:
1036  				logger.Logger.WithFields(logrus.Fields{
1037  					"swapId": swap.SwapId,
1038  					"reason": update.Status,
1039  				}).Error("Swap in failed, initiating refund")
1040  
1041  				err = svc.RefundSwap(swap.SwapId, "", true)
1042  				if err != nil {
1043  					logger.Logger.WithError(err).WithFields(logrus.Fields{
1044  						"swapId": swap.SwapId,
1045  					}).Error("Could not process refund")
1046  				}
1047  				return
1048  			}
1049  		}
1050  	}
1051  }
1052  
1053  func (svc *swapsService) startSwapOutListener(swap *db.Swap) {
1054  	updateCh := make(chan boltz.SwapUpdate, 1)
1055  	svc.swapListenersLock.Lock()
1056  	svc.swapListeners[swap.SwapId] = updateCh
1057  	svc.swapListenersLock.Unlock()
1058  
1059  	for {
1060  		err := svc.boltzWs.Subscribe([]string{swap.SwapId})
1061  		if err != nil {
1062  			logger.Logger.WithError(err).Error("Failed to subscribe to boltz websocket, retrying in 2s...")
1063  			time.Sleep(2 * time.Second)
1064  			continue
1065  		}
1066  		break
1067  	}
1068  
1069  	logger.Logger.WithField("swapId", swap.SwapId).Info("Subscribed to boltz websocket")
1070  
1071  	var err error
1072  	defer func() {
1073  		svc.swapListenersLock.Lock()
1074  		delete(svc.swapListeners, swap.SwapId)
1075  		svc.swapListenersLock.Unlock()
1076  		svc.boltzWs.Unsubscribe(swap.SwapId)
1077  		if err != nil {
1078  			logger.Logger.WithError(err).Error("Marking swap state as failed")
1079  			svc.markSwapState(swap, constants.SWAP_STATE_FAILED)
1080  		}
1081  	}()
1082  
1083  	var network *boltz.Network
1084  	network, err = boltz.ParseChain(svc.cfg.GetNetwork())
1085  	if err != nil {
1086  		logger.Logger.WithError(err).WithFields(logrus.Fields{
1087  			"swapId": swap.SwapId,
1088  		}).Error("Failed to parse network")
1089  		return
1090  	}
1091  
1092  	var ourKeys *btcec.PrivateKey
1093  	ourKeys, err = svc.keys.GetSwapKey(swap.ID)
1094  	if err != nil {
1095  		logger.Logger.WithError(err).WithFields(logrus.Fields{
1096  			"swapId": swap.SwapId,
1097  		}).Error("Failed to generate swap child private key")
1098  		return
1099  	}
1100  
1101  	var serializedTree boltz.SerializedTree
1102  	if err = json.Unmarshal(swap.SwapTree, &serializedTree); err != nil {
1103  		logger.Logger.WithError(err).WithFields(logrus.Fields{
1104  			"swapId": swap.SwapId,
1105  		}).Error("Failed to unmarshal swap tree")
1106  		return
1107  	}
1108  
1109  	boltzPubkeyBytes, _ := hex.DecodeString(swap.BoltzPubkey)
1110  
1111  	var boltzPubKey *btcec.PublicKey
1112  	boltzPubKey, err = btcec.ParsePubKey(boltzPubkeyBytes)
1113  	if err != nil {
1114  		logger.Logger.WithError(err).WithFields(logrus.Fields{
1115  			"swapId": swap.SwapId,
1116  		}).Error("Failed to parse boltz pubkey")
1117  		return
1118  	}
1119  
1120  	preimageBytes, _ := hex.DecodeString(swap.Preimage)
1121  	preimageHash := sha256.Sum256(preimageBytes)
1122  
1123  	tree := serializedTree.Deserialize()
1124  	if err = tree.Init(boltz.CurrencyBtc, true, ourKeys, boltzPubKey); err != nil {
1125  		logger.Logger.WithError(err).WithFields(logrus.Fields{
1126  			"swapId": swap.SwapId,
1127  		}).Error("Failed to initialize swap tree")
1128  		return
1129  	}
1130  
1131  	if err = tree.Check(boltz.ReverseSwap, swap.TimeoutBlockHeight, preimageHash[:]); err != nil {
1132  		logger.Logger.WithError(err).WithFields(logrus.Fields{
1133  			"swapId": swap.SwapId,
1134  		}).Error("Failed to check swap tree")
1135  		return
1136  	}
1137  
1138  	if err = tree.CheckAddress(swap.LockupAddress, network, nil); err != nil {
1139  		logger.Logger.WithError(err).WithFields(logrus.Fields{
1140  			"swapId": swap.SwapId,
1141  		}).Error("Failed to check address")
1142  		return
1143  	}
1144  
1145  	claimTicker := time.NewTicker(10 * time.Second)
1146  	defer claimTicker.Stop()
1147  
1148  	paymentErrorCh := make(chan error, 1)
1149  
1150  	for {
1151  		select {
1152  		case <-svc.ctx.Done():
1153  			logger.Logger.WithError(svc.ctx.Err()).WithFields(logrus.Fields{
1154  				"swapId": swap.SwapId,
1155  			}).Error("Swap out context cancelled")
1156  			return
1157  		case err = <-paymentErrorCh:
1158  			logger.Logger.WithError(err).WithFields(logrus.Fields{
1159  				"swapId": swap.SwapId,
1160  			}).Error("Failed to pay hold invoice, terminating swap out...")
1161  			return
1162  		case <-claimTicker.C:
1163  			if swap.ClaimTxId != "" {
1164  				confirmed, err := svc.isTransactionConfirmed(swap.ClaimTxId)
1165  				if err != nil {
1166  					logger.Logger.WithError(err).WithFields(logrus.Fields{
1167  						"swapId":    swap.SwapId,
1168  						"claimTxId": swap.ClaimTxId,
1169  					}).Debug("Claim poll failed; will retry")
1170  					break
1171  				}
1172  				if confirmed {
1173  					svc.markSwapState(swap, constants.SWAP_STATE_SUCCESS)
1174  					logger.Logger.WithField("swapId", swap.SwapId).Info("Swap succeeded")
1175  					if swap.UsedXpub {
1176  						svc.bumpAutoswapXpubIndex(swap.ID)
1177  					}
1178  					svc.eventPublisher.Publish(&events.Event{
1179  						Event: "nwc_swap_succeeded",
1180  						Properties: map[string]interface{}{
1181  							"swapType": constants.SWAP_TYPE_OUT,
1182  						},
1183  					})
1184  					return
1185  				}
1186  			}
1187  		case update, ok := <-updateCh:
1188  			if !ok {
1189  				logger.Logger.WithField("swap_id", update.Id).Error("Failed to receive update from boltz")
1190  				continue
1191  			}
1192  			if update.Id != swap.SwapId {
1193  				continue
1194  			}
1195  			switch boltz.ParseEvent(update.Status) {
1196  			case boltz.SwapCreated:
1197  				logger.Logger.WithField("swapId", swap.SwapId).Info("Paying the swap invoice")
1198  				go func() {
1199  					_, err := svc.transactionsService.LookupTransaction(svc.ctx, swap.PaymentHash, nil, svc.lnClient, nil)
1200  					if err == nil {
1201  						logger.Logger.WithField("swapId", swap.SwapId).Info("Already initiated swap invoice payment")
1202  						return
1203  					}
1204  					if !errors.Is(err, transactions.NewNotFoundError()) {
1205  						logger.Logger.WithError(err).WithField("swapId", swap.SwapId).Warn("Failed to lookup transaction")
1206  						return
1207  					}
1208  					if _, err := verifySwapOutInvoice(swap.Invoice, swap.PaymentHash, swap.SendAmountSat); err != nil {
1209  						logger.Logger.WithError(err).WithFields(logrus.Fields{
1210  							"swapId": swap.SwapId,
1211  						}).Error("Refusing to pay swap invoice")
1212  						paymentErrorCh <- err
1213  						return
1214  					}
1215  					metadata := map[string]interface{}{
1216  						"swap_id": swap.SwapId,
1217  					}
1218  					logger.Logger.WithField("swapId", swap.SwapId).Info("Initiating swap invoice payment")
1219  					_, err = svc.transactionsService.SendPaymentSync(swap.Invoice, nil, metadata, svc.lnClient, nil, nil)
1220  					if err != nil {
1221  						logger.Logger.WithError(err).WithFields(logrus.Fields{
1222  							"swapId": swap.SwapId,
1223  						}).Error("Error paying the swap invoice")
1224  						paymentErrorCh <- err
1225  						return
1226  					}
1227  				}()
1228  			case boltz.TransactionMempool, boltz.TransactionConfirmed:
1229  				logger.Logger.WithFields(logrus.Fields{
1230  					"swapId":     swap.SwapId,
1231  					"lockupTxId": update.Transaction.Id,
1232  				}).Info("Lockup transaction detected")
1233  
1234  				if swap.LockupTxId == "" {
1235  					err = svc.db.Model(swap).Updates(&db.Swap{
1236  						LockupTxId: update.Transaction.Id,
1237  					}).Error
1238  					if err != nil {
1239  						logger.Logger.WithFields(logrus.Fields{
1240  							"swapId":     swap.SwapId,
1241  							"lockupTxId": update.Transaction.Id,
1242  						}).WithError(err).Error("Failed to save lockup txid to swap")
1243  						return
1244  					}
1245  				}
1246  
1247  				if swap.ClaimTxId != "" {
1248  					logger.Logger.WithFields(logrus.Fields{
1249  						"swapId":    swap.SwapId,
1250  						"claimTxId": swap.ClaimTxId,
1251  					}).Info("Claim transaction already recorded, skipping broadcast")
1252  					continue
1253  				}
1254  				var lockupTransaction boltz.Transaction
1255  				lockupTransaction, err = boltz.NewTxFromHex(boltz.CurrencyBtc, update.Transaction.Hex, nil)
1256  				if err != nil {
1257  					logger.Logger.WithError(err).WithFields(logrus.Fields{
1258  						"swapId": swap.SwapId,
1259  					}).Error("Failed to build lockup tx from hex")
1260  					return
1261  				}
1262  
1263  				var vout uint32
1264  				vout, _, err = lockupTransaction.FindVout(network, swap.LockupAddress)
1265  				if err != nil {
1266  					logger.Logger.WithError(err).WithFields(logrus.Fields{
1267  						"swapId": swap.SwapId,
1268  					}).Error("Failed to find lockup address output")
1269  					return
1270  				}
1271  
1272  				outputs := []boltz.OutputDetails{
1273  					{
1274  						SwapId:            swap.SwapId,
1275  						SwapType:          boltz.ReverseSwap,
1276  						Address:           swap.DestinationAddress,
1277  						LockupTransaction: lockupTransaction,
1278  						Vout:              vout,
1279  						Preimage:          preimageBytes,
1280  						PrivateKey:        ourKeys,
1281  						SwapTree:          tree,
1282  						Cooperative:       true,
1283  					},
1284  				}
1285  
1286  				var boltzFee boltz.Fee
1287  				if swap.ReceiveAmountSat != 0 {
1288  					lockupAmountSat, err := lockupTransaction.VoutValue(vout)
1289  					if err != nil {
1290  						logger.Logger.WithError(err).WithFields(logrus.Fields{
1291  							"swapId": swap.SwapId,
1292  						}).Error("Failed to find lockup output value")
1293  						return
1294  					}
1295  					feeSat := lockupAmountSat - swap.ReceiveAmountSat
1296  					boltzFee.Sats = &feeSat
1297  				} else {
1298  					feeRate, err := svc.getFeeRate()
1299  					if err != nil {
1300  						logger.Logger.WithError(err).WithFields(logrus.Fields{
1301  							"swapId": swap.SwapId,
1302  						}).Error("Failed to fetch fee rate to create claim transaction")
1303  						return
1304  					}
1305  					boltzFee.SatsPerVbyte = &feeRate
1306  				}
1307  
1308  				var claimTransaction boltz.Transaction
1309  				claimTransaction, _, err = boltz.ConstructTransaction(network, boltz.CurrencyBtc, outputs, boltzFee, svc.boltzApi)
1310  				if err != nil {
1311  					logger.Logger.WithError(err).WithFields(logrus.Fields{
1312  						"swapId": swap.SwapId,
1313  					}).Error("Could not create claim transaction")
1314  					return
1315  				}
1316  
1317  				vout, _, _ = claimTransaction.FindVout(network, swap.DestinationAddress)
1318  				claimAmountSat, _ := claimTransaction.VoutValue(vout)
1319  
1320  				var txHex string
1321  				txHex, err = claimTransaction.Serialize()
1322  				if err != nil {
1323  					logger.Logger.WithError(err).WithFields(logrus.Fields{
1324  						"swapId": swap.SwapId,
1325  					}).Error("Could not serialize claim transaction")
1326  					return
1327  				}
1328  
1329  				var claimTxId string
1330  				for attempt := 1; attempt <= 5; attempt++ {
1331  					// TODO: Replace with LNClient broadcast method to avoid trusting boltz
1332  					claimTxId, err = svc.boltzApi.BroadcastTransaction(boltz.CurrencyBtc, txHex)
1333  					if err != nil {
1334  						logger.Logger.WithError(err).WithFields(logrus.Fields{
1335  							"swapId":  swap.SwapId,
1336  							"attempt": attempt,
1337  						}).Warn("Failed to broadcast transaction, retrying")
1338  						time.Sleep(1 * time.Second)
1339  						continue
1340  					}
1341  					break
1342  				}
1343  
1344  				if err != nil {
1345  					logger.Logger.WithError(err).WithFields(logrus.Fields{
1346  						"swapId": swap.SwapId,
1347  					}).Error("Could not broadcast transaction")
1348  					return
1349  				}
1350  
1351  				logger.Logger.WithFields(logrus.Fields{
1352  					"swapId":    swap.SwapId,
1353  					"claimTxId": claimTxId,
1354  				}).Info("Claim transaction broadcasted")
1355  
1356  				err = svc.db.Model(swap).Updates(&db.Swap{
1357  					ClaimTxId:        claimTxId,
1358  					ReceiveAmountSat: claimAmountSat,
1359  				}).Error
1360  				if err != nil {
1361  					logger.Logger.WithFields(logrus.Fields{
1362  						"swapId":         swap.SwapId,
1363  						"claimTxId":      claimTxId,
1364  						"claimAmountSat": claimAmountSat,
1365  					}).WithError(err).Error("Failed to save claim info to swap")
1366  					return
1367  				}
1368  			case boltz.TransactionFailed, boltz.SwapExpired:
1369  				logger.Logger.WithFields(logrus.Fields{
1370  					"swapId": swap.SwapId,
1371  					"reason": update.Status,
1372  				}).Error("Swap out failed, HTLC is cancelled")
1373  				err = errors.New(update.Status)
1374  				return
1375  			}
1376  		}
1377  	}
1378  }
1379  
1380  func (svc *swapsService) isTransactionConfirmed(txId string) (bool, error) {
1381  	transaction, err := svc.boltzApi.GetTransactionDetails(txId, boltz.CurrencyBtc)
1382  	if err != nil {
1383  		return false, err
1384  	}
1385  	return transaction.Confirmations > 0, nil
1386  }
1387  
1388  func (svc *swapsService) getFeeRate() (float64, error) {
1389  	return svc.boltzApi.GetFeeEstimation(boltz.CurrencyBtc)
1390  }
1391  
1392  func (svc *swapsService) bumpAutoswapXpubIndex(swapId uint) {
1393  	indexStr, err := svc.cfg.Get(config.AutoSwapXpubIndexStart, "")
1394  	if err != nil {
1395  		logger.Logger.Error("failed to get auto swap xpub index")
1396  		return
1397  	}
1398  	if indexStr == "" {
1399  		indexStr = "0"
1400  	}
1401  	index, err := strconv.ParseUint(indexStr, 10, 32)
1402  	if err != nil {
1403  		logger.Logger.Error("failed to parse auto swap xpub index")
1404  		return
1405  	}
1406  
1407  	err = svc.cfg.SetUpdate(config.AutoSwapXpubIndexStart, strconv.FormatUint(uint64(index+1), 10), "")
1408  	if err != nil {
1409  		logger.Logger.WithError(err).Error("Failed to update auto swap xpub index")
1410  	}
1411  	logger.Logger.WithFields(logrus.Fields{
1412  		"swapId":    swapId,
1413  		"nextIndex": index + 1,
1414  	}).Info("Updated xpub index start for swap address")
1415  }
1416  
1417  func (svc *swapsService) getChainParams() (*chaincfg.Params, error) {
1418  	var netParams *chaincfg.Params
1419  	switch svc.cfg.GetNetwork() {
1420  	case "bitcoin", "mainnet":
1421  		netParams = &chaincfg.MainNetParams
1422  	case "testnet":
1423  		netParams = &chaincfg.TestNet3Params
1424  	case "regtest":
1425  		netParams = &chaincfg.RegressionNetParams
1426  	case "signet":
1427  		netParams = &chaincfg.SigNetParams
1428  	default:
1429  		return nil, fmt.Errorf("unsupported network: %s", svc.cfg.GetNetwork())
1430  	}
1431  
1432  	return netParams, nil
1433  }
1434  
1435  func (svc *swapsService) deriveAddressFromXpub(xpub string, index uint32) (string, error) {
1436  	netParams, err := svc.getChainParams()
1437  	if err != nil {
1438  		return "", err
1439  	}
1440  
1441  	extPubKey, err := hdkeychain.NewKeyFromString(xpub)
1442  	if err != nil {
1443  		return "", fmt.Errorf("failed to parse xpub: %w", err)
1444  	}
1445  
1446  	externalChain, err := extPubKey.Derive(0)
1447  	if err != nil {
1448  		return "", fmt.Errorf("failed to derive external chain: %w", err)
1449  	}
1450  
1451  	addressKey, err := externalChain.Derive(index)
1452  	if err != nil {
1453  		return "", fmt.Errorf("failed to derive address key at index %d: %w", index, err)
1454  	}
1455  
1456  	pubKey, err := addressKey.ECPubKey()
1457  	if err != nil {
1458  		return "", fmt.Errorf("failed to get public key: %w", err)
1459  	}
1460  
1461  	pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed())
1462  	address, err := btcutil.NewAddressWitnessPubKeyHash(pubKeyHash, netParams)
1463  	if err != nil {
1464  		return "", fmt.Errorf("failed to create address: %w", err)
1465  	}
1466  
1467  	return address.EncodeAddress(), nil
1468  }
1469  
1470  func (svc *swapsService) checkAddressHasTransactions(address string, esploraApiRequester func(endpoint string) (interface{}, error)) (bool, error) {
1471  	response, err := esploraApiRequester("/address/" + address + "/txs")
1472  	if err != nil {
1473  		return false, fmt.Errorf("failed to get address transactions: %w", err)
1474  	}
1475  
1476  	transactions, ok := response.([]interface{})
1477  	if !ok {
1478  		return false, fmt.Errorf("unexpected response format from esplora API")
1479  	}
1480  
1481  	return len(transactions) > 0, nil
1482  }
1483  
1484  func (svc *swapsService) getNextUnusedAddressFromXpub() (string, error) {
1485  	// Use the decrypted XPUB from memory (already decrypted during EnableAutoSwapOut)
1486  	svc.autoSwapOutXpubLock.Lock()
1487  	destination := svc.autoSwapOutDecryptedXpub
1488  	svc.autoSwapOutXpubLock.Unlock()
1489  	if destination == "" {
1490  		return "", errors.New("no XPUB configured")
1491  	}
1492  
1493  	indexStr, err := svc.cfg.Get(config.AutoSwapXpubIndexStart, "")
1494  	if err != nil {
1495  		return "", err
1496  	}
1497  	if indexStr == "" {
1498  		indexStr = "0"
1499  	}
1500  	index, err := strconv.ParseUint(indexStr, 10, 32)
1501  	if err != nil {
1502  		return "", err
1503  	}
1504  
1505  	esploraApiRequester := func(endpoint string) (interface{}, error) {
1506  		url := svc.cfg.GetEnv().LDKEsploraServer + endpoint
1507  
1508  		client := http.Client{
1509  			Timeout: time.Second * 10,
1510  		}
1511  
1512  		req, err := http.NewRequestWithContext(svc.ctx, http.MethodGet, url, nil)
1513  		if err != nil {
1514  			return nil, err
1515  		}
1516  		res, err := client.Do(req)
1517  		if err != nil {
1518  			return nil, err
1519  		}
1520  		defer res.Body.Close()
1521  
1522  		body, err := io.ReadAll(res.Body)
1523  		if err != nil {
1524  			return nil, err
1525  		}
1526  
1527  		if res.StatusCode != http.StatusOK {
1528  			logger.Logger.WithFields(logrus.Fields{
1529  				"endpoint":    endpoint,
1530  				"status_code": res.StatusCode,
1531  				"body":        string(body),
1532  			}).Error("Swaps esplora endpoint returned non-success code")
1533  			return nil, fmt.Errorf("swaps esplora endpoint returned non-success code: %s", string(body))
1534  		}
1535  
1536  		var jsonContent interface{}
1537  		err = json.Unmarshal(body, &jsonContent)
1538  		if err != nil {
1539  			return nil, err
1540  		}
1541  		return jsonContent, nil
1542  	}
1543  
1544  	const addressLookAheadLimit = 100
1545  
1546  	for i := uint32(index); i < uint32(index)+addressLookAheadLimit; i++ {
1547  		address, err := svc.deriveAddressFromXpub(destination, i)
1548  		if err != nil {
1549  			return "", fmt.Errorf("failed to derive address at index %d: %w", i, err)
1550  		}
1551  
1552  		hasTransactions, err := svc.checkAddressHasTransactions(address, esploraApiRequester)
1553  		if err != nil {
1554  			return "", fmt.Errorf("failed to check address for transactions at index %d: %w", i, err)
1555  		}
1556  
1557  		if !hasTransactions {
1558  			return address, nil
1559  		}
1560  	}
1561  
1562  	return "", fmt.Errorf("could not find unused address within %d addresses starting from index %d", addressLookAheadLimit, index)
1563  }
1564  
1565  func (svc *swapsService) ValidateAddress(address string) error {
1566  	netParams, err := svc.getChainParams()
1567  	if err != nil {
1568  		return err
1569  	}
1570  
1571  	_, err = btcutil.DecodeAddress(address, netParams)
1572  	if err != nil {
1573  		return fmt.Errorf("invalid bitcoin address: %w", err)
1574  	}
1575  
1576  	return nil
1577  }
1578  
1579  func (svc *swapsService) ValidateXpub(xpub string) error {
1580  	extendedKey, err := hdkeychain.NewKeyFromString(xpub)
1581  	if err != nil {
1582  		return fmt.Errorf("invalid xpub: %w", err)
1583  	}
1584  
1585  	if extendedKey.IsPrivate() {
1586  		return fmt.Errorf("private extended key not allowed")
1587  	}
1588  
1589  	return nil
1590  }
1591  
1592  func (svc *swapsService) GetDecryptedAutoSwapXpub() string {
1593  	svc.autoSwapOutXpubLock.Lock()
1594  	defer svc.autoSwapOutXpubLock.Unlock()
1595  
1596  	return svc.autoSwapOutDecryptedXpub
1597  }
1598