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