transactions_service.go raw
1 package transactions
2
3 import (
4 "context"
5 "crypto/rand"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10 "fmt"
11 "math"
12 "regexp"
13 "slices"
14 "strconv"
15 "strings"
16 "sync"
17 "time"
18
19 decodepay "github.com/nbd-wtf/ln-decodepay"
20 "github.com/sirupsen/logrus"
21 "gorm.io/datatypes"
22 "gorm.io/gorm"
23 "gorm.io/gorm/clause"
24
25 "github.com/getAlby/hub/constants"
26 "github.com/getAlby/hub/db"
27 "github.com/getAlby/hub/db/queries"
28 "github.com/getAlby/hub/events"
29 "github.com/getAlby/hub/lnclient"
30 "github.com/getAlby/hub/logger"
31 )
32
33 type transactionsService struct {
34 db *gorm.DB
35 eventPublisher events.EventPublisher
36 }
37
38 type TransactionsService interface {
39 events.EventSubscriber
40 MakeInvoice(ctx context.Context, amountMsat uint64, description string, descriptionHash string, expiry uint64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint, throughNodePubkey *string) (*Transaction, error)
41 LookupTransaction(ctx context.Context, paymentHash string, transactionType *string, lnClient lnclient.LNClient, appId *uint) (*Transaction, error)
42 ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaidOutgoing bool, unpaidIncoming bool, lnClient lnclient.LNClient, appId *uint, forceFilterByAppId bool, filters *ListTransactionsFilters) (transactions []Transaction, totalCount uint64, err error)
43 SendPaymentSync(payReq string, amountMsat *uint64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error)
44 SendKeysend(amountMsat uint64, destination string, customRecords []lnclient.TLVRecord, preimage string, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error)
45 MakeHoldInvoice(ctx context.Context, amountMsat uint64, description string, descriptionHash string, expiry uint64, paymentHash string, minCltvExpiryDelta *uint64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error)
46 SettleHoldInvoice(ctx context.Context, preimage string, lnClient lnclient.LNClient) (*Transaction, error)
47 CancelHoldInvoice(ctx context.Context, paymentHash string, lnClient lnclient.LNClient) error
48 SetTransactionMetadata(ctx context.Context, id uint, metadata map[string]interface{}) error
49 SetTransactionUserLabels(ctx context.Context, id uint, labels map[string]string) error
50 }
51
52 const (
53 BoostagramTlvType = 7629169
54 WhatsatTlvType = 34349334
55 CustomKeyTlvType = 696969
56 )
57
58 // Prevent races when checking the current balance and creating payment
59 // transactions from concurrent goroutines.
60 var balanceValidationLock = &sync.Mutex{}
61
62 type Transaction = db.Transaction
63
64 type ListTransactionsFilters struct {
65 Type *string
66 MinAmountMsat *uint64
67 HideFailed bool
68 SearchTerm string
69 }
70
71 var paymentHashRegex = regexp.MustCompile("^[0-9a-f]{64}$")
72
73 // escapeLikePattern makes a string match literally in a LIKE ... ESCAPE '\'
74 // clause by escaping the wildcard characters % and _. This is not an SQL
75 // injection concern (search terms are always passed as bound parameters);
76 // without it a term like "50%" would behave as a wildcard pattern.
77 // The backslash must be escaped first.
78 func escapeLikePattern(s string) string {
79 s = strings.ReplaceAll(s, `\`, `\\`)
80 s = strings.ReplaceAll(s, "%", `\%`)
81 s = strings.ReplaceAll(s, "_", `\_`)
82 return s
83 }
84
85 type Boostagram struct {
86 AppName string `json:"app_name"`
87 Name string `json:"name"`
88 Podcast string `json:"podcast"`
89 URL string `json:"url"`
90 Episode StringOrNumber `json:"episode,omitempty"`
91 FeedId StringOrNumber `json:"feedID,omitempty"`
92 ItemId StringOrNumber `json:"itemID,omitempty"`
93 Timestamp int64 `json:"ts,omitempty"`
94 Message string `json:"message,omitempty"`
95 SenderId StringOrNumber `json:"sender_id"`
96 SenderName string `json:"sender_name"`
97 Time string `json:"time"`
98 Action string `json:"action"`
99 ValueMsatTotal int64 `json:"value_msat_total"`
100 }
101
102 type StringOrNumber struct {
103 StringData string
104 NumberData int64
105 }
106
107 func (sn *StringOrNumber) UnmarshalJSON(data []byte) error {
108 if err := json.Unmarshal(data, &sn.StringData); err == nil {
109 return nil
110 }
111
112 if err := json.Unmarshal(data, &sn.NumberData); err == nil {
113 return nil
114 }
115
116 return fmt.Errorf("cannot unmarshal %s into StringOrNumber type", data)
117 }
118
119 func (sn StringOrNumber) String() string {
120 if sn.StringData != "" {
121 return sn.StringData
122 }
123 return fmt.Sprintf("%d", sn.NumberData)
124 }
125
126 type notFoundError struct {
127 }
128
129 func NewNotFoundError() error {
130 return ¬FoundError{}
131 }
132
133 func (err *notFoundError) Error() string {
134 return "The transaction requested was not found"
135 }
136
137 type insufficientBalanceError struct {
138 }
139
140 func NewInsufficientBalanceError() error {
141 return &insufficientBalanceError{}
142 }
143
144 func (err *insufficientBalanceError) Error() string {
145 return "Insufficient balance remaining to make the requested payment"
146 }
147
148 type quotaExceededError struct {
149 }
150
151 func NewQuotaExceededError() error {
152 return "aExceededError{}
153 }
154
155 func (err *quotaExceededError) Error() string {
156 return "Your app does not have enough budget remaining to make this payment. Please review this app in the connections page of your Alby Hub."
157 }
158
159 func NewTransactionsService(db *gorm.DB, eventPublisher events.EventPublisher) *transactionsService {
160 return &transactionsService{
161 db: db,
162 eventPublisher: eventPublisher,
163 }
164 }
165
166 func (svc *transactionsService) MakeInvoice(ctx context.Context, amountMsat uint64, description string, descriptionHash string, expiry uint64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint, throughNodePubkey *string) (*Transaction, error) {
167 logger.Logger.WithFields(logrus.Fields{
168 "app_id": appId,
169 "request_event_id": requestEventId,
170 "amount_msat": amountMsat,
171 "description": description,
172 "description_hash": descriptionHash,
173 "expiry": expiry,
174 "metadata": metadata,
175 }).Debug("Making invoice")
176
177 if amountMsat%1000 != 0 {
178 return nil, errors.New("the amount must be a whole number of satoshis")
179 }
180 if amountMsat < 1000 {
181 return nil, errors.New("the amount must be at least 1 satoshi")
182 }
183
184 var metadataBytes []byte
185 if metadata != nil {
186 var err error
187 metadataBytes, err = json.Marshal(metadata)
188 if err != nil {
189 logger.Logger.WithError(err).Error("Failed to serialize metadata")
190 return nil, err
191 }
192 if len(metadataBytes) > constants.INVOICE_METADATA_MAX_LENGTH {
193 return nil, fmt.Errorf("encoded invoice metadata provided is too large. Limit: %d Received: %d", constants.INVOICE_METADATA_MAX_LENGTH, len(metadataBytes))
194 }
195 }
196
197 if metadata["app_id"] != nil {
198 overwriteAppIdType, ok := metadata["app_id"].(float64)
199 if !ok {
200 return nil, errors.New("failed to overwrite app ID")
201 }
202 overwriteAppId := uint(overwriteAppIdType)
203 logger.Logger.WithField("app_id", overwriteAppId).Info("Making invoice with overwritten app ID")
204 appId = &overwriteAppId
205 }
206
207 lnClientTransaction, err := lnClient.MakeInvoice(ctx, int64(amountMsat), description, descriptionHash, int64(expiry), throughNodePubkey)
208 if err != nil {
209 logger.Logger.WithError(err).Error("Failed to create transaction")
210 return nil, err
211 }
212
213 var preimage *string
214 if lnClientTransaction.Preimage != "" {
215 preimage = &lnClientTransaction.Preimage
216 }
217
218 var expiresAt *time.Time
219 if lnClientTransaction.ExpiresAt != nil {
220 expiresAtValue := time.Unix(*lnClientTransaction.ExpiresAt, 0)
221 expiresAt = &expiresAtValue
222 }
223
224 dbTransaction := db.Transaction{
225 AppId: appId,
226 RequestEventId: requestEventId,
227 Type: lnClientTransaction.Type,
228 State: constants.TRANSACTION_STATE_PENDING,
229 AmountMsat: uint64(lnClientTransaction.AmountMsat),
230 FeeMsat: uint64(max(lnClientTransaction.FeesPaidMsat, 0)),
231 Description: description,
232 DescriptionHash: descriptionHash,
233 PaymentRequest: lnClientTransaction.Invoice,
234 PaymentHash: lnClientTransaction.PaymentHash,
235 ExpiresAt: expiresAt,
236 Preimage: preimage,
237 Metadata: datatypes.JSON(metadataBytes),
238 }
239 err = svc.db.Create(&dbTransaction).Error
240 if err != nil {
241 logger.Logger.WithError(err).Error("Failed to create DB transaction")
242 return nil, err
243 }
244 return &dbTransaction, nil
245 }
246
247 func (svc *transactionsService) MakeHoldInvoice(ctx context.Context, amountMsat uint64, description string, descriptionHash string, expiry uint64, paymentHash string, minCltvExpiryDelta *uint64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) {
248 var err error
249 var metadataBytes []byte
250 if metadata != nil {
251 metadataBytes, err = json.Marshal(metadata)
252 if err != nil {
253 logger.Logger.WithError(err).Error("Failed to serialize metadata")
254 return nil, err
255 }
256 if len(metadataBytes) > constants.INVOICE_METADATA_MAX_LENGTH {
257 return nil, fmt.Errorf("encoded invoice metadata provided is too large. Limit: %d Received: %d", constants.INVOICE_METADATA_MAX_LENGTH, len(metadataBytes))
258 }
259 }
260
261 lnClientTransaction, err := lnClient.MakeHoldInvoice(ctx, int64(amountMsat), description, descriptionHash, int64(expiry), paymentHash, minCltvExpiryDelta)
262 if err != nil {
263 logger.Logger.WithError(err).Error("Failed to create hold invoice via LN client")
264 return nil, err
265 }
266
267 var preimage *string
268 if lnClientTransaction.Preimage != "" {
269 preimage = &lnClientTransaction.Preimage
270 }
271
272 var expiresAt *time.Time
273 if lnClientTransaction.ExpiresAt != nil {
274 expiresAtValue := time.Unix(*lnClientTransaction.ExpiresAt, 0)
275 expiresAt = &expiresAtValue
276 }
277
278 dbTransaction := db.Transaction{
279 AppId: appId,
280 RequestEventId: requestEventId,
281 Type: constants.TRANSACTION_TYPE_INCOMING,
282 State: constants.TRANSACTION_STATE_PENDING,
283 AmountMsat: uint64(lnClientTransaction.AmountMsat),
284 Description: description,
285 DescriptionHash: descriptionHash,
286 PaymentRequest: lnClientTransaction.Invoice,
287 PaymentHash: lnClientTransaction.PaymentHash,
288 ExpiresAt: expiresAt,
289 Preimage: preimage,
290 Metadata: datatypes.JSON(metadataBytes),
291 Hold: true,
292 }
293 err = svc.db.Create(&dbTransaction).Error
294 if err != nil {
295 logger.Logger.WithError(err).Error("Failed to create hold invoice DB transaction")
296 return nil, err
297 }
298 return &dbTransaction, nil
299 }
300
301 func (svc *transactionsService) SendPaymentSync(payReq string, amountMsat *uint64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) {
302 var metadataBytes []byte
303 if metadata != nil {
304 var err error
305 metadataBytes, err = json.Marshal(metadata)
306 if err != nil {
307 logger.Logger.WithError(err).Error("Failed to serialize metadata")
308 return nil, err
309 }
310 if len(metadataBytes) > constants.INVOICE_METADATA_MAX_LENGTH {
311 return nil, fmt.Errorf("encoded payment metadata provided is too large. Limit: %d Received: %d", constants.INVOICE_METADATA_MAX_LENGTH, len(metadataBytes))
312 }
313 }
314
315 payReq = strings.ToLower(payReq)
316 paymentRequest, err := decodepay.Decodepay(payReq)
317 if err != nil {
318 logger.Logger.WithFields(logrus.Fields{
319 "bolt11": payReq,
320 }).Errorf("Failed to decode bolt11 invoice: %v", err)
321
322 return nil, err
323 }
324
325 if time.Now().After(time.Unix(int64(paymentRequest.CreatedAt+paymentRequest.Expiry), 0)) {
326 logger.Logger.WithFields(logrus.Fields{
327 "bolt11": payReq,
328 "expiry": time.Unix(int64(paymentRequest.CreatedAt+paymentRequest.Expiry), 0),
329 }).Errorf("this invoice has expired")
330
331 return nil, errors.New("this invoice has expired")
332 }
333
334 // A payment is a self-payment if we have an incoming transaction for the
335 // exact same invoice (i.e. it was generated by this hub).
336 selfPayment := false
337 var incomingTransaction db.Transaction
338 result := svc.db.Limit(1).Find(&incomingTransaction, &db.Transaction{
339 Type: constants.TRANSACTION_TYPE_INCOMING,
340 // NOTE: filter by payment hash so the payment hash index is used,
341 // but also match the payment request as wrapped invoices share
342 // the same hash but have different payment requests
343 PaymentHash: paymentRequest.PaymentHash,
344 PaymentRequest: payReq,
345 })
346 if result.Error == nil && result.RowsAffected > 0 {
347 selfPayment = true
348 }
349
350 var dbTransaction db.Transaction
351
352 paymentAmountMsat := uint64(paymentRequest.MSatoshi)
353 if amountMsat != nil && paymentRequest.MSatoshi == 0 {
354 paymentAmountMsat = *amountMsat
355 }
356
357 err = func() error {
358 balanceValidationLock.Lock()
359 defer balanceValidationLock.Unlock()
360 return svc.db.Transaction(func(tx *gorm.DB) error {
361 var existingSettledTransaction db.Transaction
362 if tx.Limit(1).Find(&existingSettledTransaction, &db.Transaction{
363 Type: constants.TRANSACTION_TYPE_OUTGOING,
364 PaymentHash: paymentRequest.PaymentHash,
365 PaymentRequest: payReq,
366 State: constants.TRANSACTION_STATE_SETTLED,
367 }).RowsAffected > 0 {
368 logger.Logger.WithFields(logrus.Fields{
369 "payment_request": payReq,
370 "payment_hash": paymentRequest.PaymentHash,
371 }).Debug("this invoice has already been paid")
372 return errors.New("this invoice has already been paid")
373 }
374 if tx.Limit(1).Find(&existingSettledTransaction, &db.Transaction{
375 Type: constants.TRANSACTION_TYPE_OUTGOING,
376 PaymentHash: paymentRequest.PaymentHash,
377 PaymentRequest: payReq,
378 State: constants.TRANSACTION_STATE_PENDING,
379 }).RowsAffected > 0 {
380 logger.Logger.WithFields(logrus.Fields{
381 "payment_request": payReq,
382 "payment_hash": paymentRequest.PaymentHash,
383 }).Debug("this invoice is already being paid")
384 return errors.New("there is already a payment pending for this invoice")
385 }
386
387 err := svc.validateCanPay(tx, appId, paymentAmountMsat, paymentRequest.Description, selfPayment)
388 if err != nil {
389 return err
390 }
391
392 var expiresAt *time.Time
393 if paymentRequest.Expiry > 0 {
394 expiresAtValue := time.Now().Add(time.Duration(paymentRequest.Expiry) * time.Second)
395 expiresAt = &expiresAtValue
396 }
397 dbTransaction = db.Transaction{
398 AppId: appId,
399 RequestEventId: requestEventId,
400 Type: constants.TRANSACTION_TYPE_OUTGOING,
401 State: constants.TRANSACTION_STATE_PENDING,
402 FeeReserveMsat: CalculateFeeReserveMsat(paymentAmountMsat),
403 AmountMsat: paymentAmountMsat,
404 PaymentRequest: payReq,
405 PaymentHash: paymentRequest.PaymentHash,
406 Description: paymentRequest.Description,
407 DescriptionHash: paymentRequest.DescriptionHash,
408 ExpiresAt: expiresAt,
409 SelfPayment: selfPayment,
410 Metadata: datatypes.JSON(metadataBytes),
411 }
412 err = tx.Create(&dbTransaction).Error
413 return err
414 })
415 }()
416
417 if err != nil {
418 logger.Logger.WithFields(logrus.Fields{
419 "bolt11": payReq,
420 }).WithError(err).Error("Failed to create DB transaction")
421 return nil, err
422 }
423
424 logger.Logger.WithFields(logrus.Fields{
425 "app_id": appId,
426 "request_event_id": requestEventId,
427 "amount_msat": paymentAmountMsat,
428 "description": paymentRequest.Description,
429 "description_hash": paymentRequest.DescriptionHash,
430 "expiry": paymentRequest.Expiry,
431 "self_payment": selfPayment,
432 "metadata": metadata,
433 }).Debug("Initiating payment")
434
435 var response *lnclient.PayInvoiceResponse
436 if selfPayment {
437 response, err = svc.interceptSelfPayment(payReq, paymentRequest.PaymentHash, lnClient)
438 } else {
439 response, err = lnClient.SendPaymentSync(payReq, amountMsat)
440 }
441
442 if err != nil {
443 logger.Logger.WithFields(logrus.Fields{
444 "bolt11": payReq,
445 }).WithError(err).Error("Failed to send payment")
446
447 if _, markFailedErr := svc.markPaymentFailed(&dbTransaction, err.Error()); markFailedErr != nil {
448 logger.Logger.WithFields(logrus.Fields{
449 "bolt11": payReq,
450 }).WithError(markFailedErr).Error("Failed to mark payment as failed")
451 }
452
453 return nil, err
454 }
455
456 // the payment definitely succeeded
457 settledTransaction, err := svc.markTransactionSettled(&dbTransaction, response.Preimage, response.FeeMsat, selfPayment)
458 if err != nil {
459 return nil, err
460 }
461
462 return settledTransaction, nil
463 }
464
465 func (svc *transactionsService) SendKeysend(amountMsat uint64, destination string, customRecords []lnclient.TLVRecord, preimage string, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) {
466 if preimage == "" {
467 preImageBytes, err := makePreimageHex()
468 if err != nil {
469 return nil, err
470 }
471 preimage = hex.EncodeToString(preImageBytes)
472 }
473
474 preImageBytes, err := hex.DecodeString(preimage)
475 if err != nil || len(preImageBytes) != 32 {
476 logger.Logger.WithFields(logrus.Fields{
477 "preimage": preimage,
478 }).WithError(err).Error("Invalid preimage")
479 return nil, err
480 }
481
482 paymentHash256 := sha256.New()
483 paymentHash256.Write(preImageBytes)
484 paymentHashBytes := paymentHash256.Sum(nil)
485 paymentHash := hex.EncodeToString(paymentHashBytes)
486
487 metadata := map[string]interface{}{}
488
489 metadata["destination"] = destination
490
491 metadata["tlv_records"] = customRecords
492 metadataBytes, err := json.Marshal(metadata)
493 if err != nil {
494 logger.Logger.WithError(err).Error("Failed to serialize transaction metadata")
495 return nil, err
496 }
497 boostagramBytes := svc.getBoostagramBytesFromCustomRecords(customRecords)
498
499 var dbTransaction db.Transaction
500
501 selfPayment := destination == lnClient.GetPubkey()
502
503 err = func() error {
504 balanceValidationLock.Lock()
505 defer balanceValidationLock.Unlock()
506 return svc.db.Transaction(func(tx *gorm.DB) error {
507 err := svc.validateCanPay(tx, appId, amountMsat, "", selfPayment)
508 if err != nil {
509 return err
510 }
511
512 dbTransaction = db.Transaction{
513 AppId: appId,
514 Description: svc.getDescriptionFromCustomRecords(customRecords),
515 RequestEventId: requestEventId,
516 Type: constants.TRANSACTION_TYPE_OUTGOING,
517 State: constants.TRANSACTION_STATE_PENDING,
518 FeeReserveMsat: CalculateFeeReserveMsat(uint64(amountMsat)),
519 AmountMsat: amountMsat,
520 Metadata: datatypes.JSON(metadataBytes),
521 Boostagram: datatypes.JSON(boostagramBytes),
522 PaymentHash: paymentHash,
523 Preimage: &preimage,
524 SelfPayment: selfPayment,
525 }
526 err = tx.Create(&dbTransaction).Error
527
528 return err
529 })
530 }()
531
532 if err != nil {
533 logger.Logger.WithFields(logrus.Fields{
534 "destination": destination,
535 "amount_msat": amountMsat,
536 }).WithError(err).Error("Failed to create DB transaction")
537 return nil, err
538 }
539
540 var payKeysendResponse *lnclient.PayKeysendResponse
541
542 if selfPayment {
543 // for keysend self-payments we need to create an incoming payment at the time of the payment
544 recipientAppId := svc.getAppIdFromCustomRecords(customRecords, svc.db)
545 dbTransaction := db.Transaction{
546 AppId: recipientAppId,
547 RequestEventId: nil, // it is related to this request but for a different app
548 Type: constants.TRANSACTION_TYPE_INCOMING,
549 State: constants.TRANSACTION_STATE_PENDING,
550 AmountMsat: amountMsat,
551 PaymentHash: paymentHash,
552 Preimage: &preimage,
553 Description: svc.getDescriptionFromCustomRecords(customRecords),
554 Metadata: datatypes.JSON(metadataBytes),
555 Boostagram: datatypes.JSON(boostagramBytes),
556 SelfPayment: true,
557 }
558 err = svc.db.Create(&dbTransaction).Error
559 if err != nil {
560 logger.Logger.WithError(err).Error("Failed to create DB transaction")
561 return nil, err
562 }
563
564 _, err = svc.interceptSelfPayment("", paymentHash, lnClient)
565 if err == nil {
566 payKeysendResponse = &lnclient.PayKeysendResponse{
567 FeeMsat: 0,
568 }
569 }
570 } else {
571 payKeysendResponse, err = lnClient.SendKeysend(amountMsat, destination, customRecords, preimage)
572 }
573
574 if err != nil {
575 logger.Logger.WithFields(logrus.Fields{
576 "destination": destination,
577 "amount_msat": amountMsat,
578 }).WithError(err).Error("Failed to send payment")
579
580 if _, markFailedErr := svc.markPaymentFailed(&dbTransaction, err.Error()); markFailedErr != nil {
581 logger.Logger.WithFields(logrus.Fields{
582 "destination": destination,
583 "amount_msat": amountMsat,
584 }).WithError(markFailedErr).Error("Failed to mark payment as failed")
585 }
586
587 return nil, err
588 }
589
590 // the payment definitely succeeded
591 settledTransaction, err := svc.markTransactionSettled(&dbTransaction, preimage, payKeysendResponse.FeeMsat, selfPayment)
592 if err != nil {
593 return nil, err
594 }
595
596 return settledTransaction, nil
597 }
598
599 func (svc *transactionsService) LookupTransaction(ctx context.Context, paymentHash string, transactionType *string, lnClient lnclient.LNClient, appId *uint) (*Transaction, error) {
600 transaction := db.Transaction{}
601
602 tx := svc.db
603
604 var isIsolatedApp bool
605 if appId != nil {
606 err := svc.db.
607 Model(&db.App{}).
608 Where("id", *appId).
609 Pluck("isolated", &isIsolatedApp).
610 Error
611 if err != nil {
612 if errors.Is(err, gorm.ErrRecordNotFound) {
613 return nil, NewNotFoundError()
614 }
615 return nil, err
616 }
617 }
618
619 if isIsolatedApp {
620 tx = tx.Where("app_id = ?", *appId)
621 }
622
623 if transactionType != nil {
624 tx = tx.Where("type = ?", *transactionType)
625 }
626
627 // order settled first, otherwise by created date, as there can be multiple outgoing payments
628 // for the same payment hash (if you tried to pay an invoice multiple times - e.g. the first time failed)
629 result := tx.Order("settled_at desc, created_at desc").Limit(1).Find(&transaction, &db.Transaction{
630 // Type: transactionType,
631 PaymentHash: paymentHash,
632 })
633
634 if result.Error != nil {
635 logger.Logger.WithError(result.Error).Error("Failed to lookup transaction")
636 return nil, result.Error
637 }
638
639 if result.RowsAffected == 0 {
640 logger.Logger.WithFields(logrus.Fields{
641 "payment_hash": paymentHash,
642 "app_id": appId,
643 }).WithError(result.Error).Error("transaction not found")
644 return nil, NewNotFoundError()
645 }
646
647 if transaction.State == constants.TRANSACTION_STATE_PENDING {
648 svc.checkUnsettledTransaction(ctx, &transaction, lnClient)
649 }
650
651 return &transaction, nil
652 }
653
654 func (svc *transactionsService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaidOutgoing bool, unpaidIncoming bool, lnClient lnclient.LNClient, appId *uint, forceFilterByAppId bool, filters *ListTransactionsFilters) (transactions []Transaction, totalCount uint64, err error) {
655 svc.checkUnsettledTransactions(ctx, lnClient)
656
657 var isIsolatedApp bool
658 if appId != nil {
659 err := svc.db.
660 Model(&db.App{}).
661 Where("id", *appId).
662 Pluck("isolated", &isIsolatedApp).
663 Error
664 if err != nil {
665 if errors.Is(err, gorm.ErrRecordNotFound) {
666 return nil, 0, NewNotFoundError()
667 }
668 return nil, 0, err
669 }
670 }
671
672 tx := svc.db
673
674 if isIsolatedApp || forceFilterByAppId {
675 tx = tx.Where("app_id = ?", *appId)
676 }
677
678 if !unpaidOutgoing && !unpaidIncoming {
679 tx = tx.Where("state = ?", constants.TRANSACTION_STATE_SETTLED)
680 } else if unpaidOutgoing && !unpaidIncoming {
681 tx = tx.Where("state = ? OR type = ?", constants.TRANSACTION_STATE_SETTLED, constants.TRANSACTION_TYPE_OUTGOING)
682 } else if unpaidIncoming && !unpaidOutgoing {
683 tx = tx.Where("state = ? OR type = ?", constants.TRANSACTION_STATE_SETTLED, constants.TRANSACTION_TYPE_INCOMING)
684 }
685
686 if filters != nil {
687 if filters.Type != nil {
688 tx = tx.Where("type = ?", *filters.Type)
689 }
690 if filters.MinAmountMsat != nil {
691 tx = tx.Where("amount_msat >= ?", *filters.MinAmountMsat)
692 }
693 if filters.HideFailed {
694 tx = tx.Where("state != ?", constants.TRANSACTION_STATE_FAILED)
695 }
696 if searchTerm := strings.ToLower(strings.TrimSpace(filters.SearchTerm)); searchTerm != "" {
697 likePattern := "%" + escapeLikePattern(searchTerm) + "%"
698 labelsCondition := `EXISTS (SELECT 1 FROM json_each(transactions.metadata, '$.user_labels') AS user_labels WHERE LOWER(user_labels.key) LIKE ? ESCAPE '\' OR LOWER(user_labels.value) LIKE ? ESCAPE '\')`
699 if svc.db.Dialector.Name() == "postgres" {
700 labelsCondition = `EXISTS (SELECT 1 FROM jsonb_each_text((transactions.metadata->'user_labels')::jsonb) AS user_labels WHERE LOWER(user_labels.key) LIKE ? ESCAPE '\' OR LOWER(user_labels.value) LIKE ? ESCAPE '\')`
701 }
702 conditions := `LOWER(description) LIKE ? ESCAPE '\' OR ` + labelsCondition
703 args := []interface{}{likePattern, likePattern, likePattern}
704
705 paymentHash := ""
706 if paymentHashRegex.MatchString(searchTerm) {
707 paymentHash = searchTerm
708 } else if strings.HasPrefix(searchTerm, "ln") {
709 if paymentRequest, err := decodepay.Decodepay(searchTerm); err == nil {
710 paymentHash = strings.ToLower(paymentRequest.PaymentHash)
711 }
712 }
713 if paymentHash != "" {
714 conditions += " OR payment_hash = ?"
715 args = append(args, paymentHash)
716 }
717
718 tx = tx.Where(conditions, args...)
719 }
720 }
721
722 if from > 0 {
723 tx = tx.Where("updated_at >= ?", time.Unix(int64(from), 0))
724 }
725 if until > 0 {
726 tx = tx.Where("updated_at <= ?", time.Unix(int64(until), 0))
727 }
728
729 var totalCount64 int64
730 result := tx.Model(&db.Transaction{}).Count(&totalCount64)
731 if result.Error != nil {
732 logger.Logger.WithError(result.Error).Error("Failed to count DB transactions")
733 return nil, 0, result.Error
734 }
735 totalCount = uint64(totalCount64)
736
737 tx = tx.Order("updated_at desc")
738
739 if limit > 0 {
740 tx = tx.Limit(int(limit))
741 }
742 if offset > 0 {
743 tx = tx.Offset(int(offset))
744 }
745
746 result = tx.Find(&transactions)
747 if result.Error != nil {
748 logger.Logger.WithError(result.Error).Error("Failed to list DB transactions")
749 return nil, 0, result.Error
750 }
751
752 return transactions, totalCount, nil
753 }
754
755 func (svc *transactionsService) checkUnsettledTransactions(ctx context.Context, lnClient lnclient.LNClient) {
756 // Only check unsettled transactions for clients that don't support async events
757 // checkUnsettledTransactions does not work for keysend payments!
758 if slices.Contains(lnClient.GetSupportedNIP47NotificationTypes(), "payment_received") {
759 return
760 }
761
762 // check pending payments less than a day old
763 transactions := []Transaction{}
764 result := svc.db.Where("state = ? AND created_at > ?", constants.TRANSACTION_STATE_PENDING, time.Now().Add(-24*time.Hour)).Find(&transactions)
765 if result.Error != nil {
766 logger.Logger.WithError(result.Error).Error("Failed to list DB transactions")
767 return
768 }
769 for _, transaction := range transactions {
770 svc.checkUnsettledTransaction(ctx, &transaction, lnClient)
771 }
772 }
773 func (svc *transactionsService) checkUnsettledTransaction(ctx context.Context, transaction *db.Transaction, lnClient lnclient.LNClient) {
774 if slices.Contains(lnClient.GetSupportedNIP47NotificationTypes(), "payment_received") {
775 return
776 }
777
778 lnClientTransaction, err := lnClient.LookupInvoice(ctx, transaction.PaymentHash)
779 if err != nil {
780 logger.Logger.WithFields(logrus.Fields{
781 "bolt11": transaction.PaymentRequest,
782 }).WithError(err).Error("Failed to check transaction")
783 return
784 }
785 // update transaction state
786 if lnClientTransaction.SettledAt != nil {
787 _, err = svc.markTransactionSettled(transaction, lnClientTransaction.Preimage, uint64(lnClientTransaction.FeesPaidMsat), false)
788 if err != nil {
789 logger.Logger.WithError(err).Error("Failed to mark payment sent when checking unsettled transaction")
790 }
791 }
792 }
793
794 func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) {
795 switch event.Event {
796 case "nwc_lnclient_payment_received":
797 lnClientTransaction, ok := event.Properties.(*lnclient.Transaction)
798 if !ok {
799 logger.Logger.WithField("event", event).Error("Failed to cast event")
800 return
801 }
802
803 var dbTransaction db.Transaction
804 result := svc.db.Limit(1).Find(&dbTransaction, &db.Transaction{
805 Type: constants.TRANSACTION_TYPE_INCOMING,
806 PaymentHash: lnClientTransaction.PaymentHash,
807 })
808
809 if result.Error != nil {
810 logger.Logger.WithFields(logrus.Fields{
811 "payment_hash": lnClientTransaction.PaymentHash,
812 }).WithError(result.Error).Error("Failed to find transaction")
813 return
814 }
815
816 if result.RowsAffected == 0 {
817 var appId *uint
818 description := lnClientTransaction.Description
819 var metadataBytes []byte
820 var boostagramBytes []byte
821 if lnClientTransaction.Metadata != nil {
822 var err error
823 metadataBytes, err = json.Marshal(lnClientTransaction.Metadata)
824 if err != nil {
825 logger.Logger.WithError(err).Error("Failed to serialize transaction metadata")
826 return
827 }
828
829 var customRecords []lnclient.TLVRecord
830 customRecords, _ = lnClientTransaction.Metadata["tlv_records"].([]lnclient.TLVRecord)
831 boostagramBytes = svc.getBoostagramBytesFromCustomRecords(customRecords)
832 extractedDescription := svc.getDescriptionFromCustomRecords(customRecords)
833 if extractedDescription != "" {
834 description = extractedDescription
835 }
836 // find app by custom key/value records
837 appId = svc.getAppIdFromCustomRecords(customRecords, svc.db)
838 }
839 var expiresAt *time.Time
840 if lnClientTransaction.ExpiresAt != nil {
841 expiresAtValue := time.Unix(*lnClientTransaction.ExpiresAt, 0)
842 expiresAt = &expiresAtValue
843 }
844 dbTransaction = db.Transaction{
845 Type: constants.TRANSACTION_TYPE_INCOMING,
846 AmountMsat: uint64(lnClientTransaction.AmountMsat),
847 PaymentRequest: lnClientTransaction.Invoice,
848 PaymentHash: lnClientTransaction.PaymentHash,
849 Description: description,
850 DescriptionHash: lnClientTransaction.DescriptionHash,
851 ExpiresAt: expiresAt,
852 Metadata: datatypes.JSON(metadataBytes),
853 Boostagram: datatypes.JSON(boostagramBytes),
854 AppId: appId,
855 }
856 if _, err := svc.createSettledTransactionFromNotification(&dbTransaction, lnClientTransaction.Preimage, uint64(lnClientTransaction.FeesPaidMsat), false); err != nil {
857 logger.Logger.WithFields(logrus.Fields{
858 "payment_hash": lnClientTransaction.PaymentHash,
859 }).WithError(err).Error("Failed to create settled transaction")
860 }
861 return
862 }
863
864 if _, err := svc.markTransactionSettled(&dbTransaction, lnClientTransaction.Preimage, uint64(lnClientTransaction.FeesPaidMsat), false); err != nil {
865 logger.Logger.WithFields(logrus.Fields{
866 "payment_hash": lnClientTransaction.PaymentHash,
867 }).WithError(err).Error("Failed to mark transaction as settled")
868 }
869
870 case "nwc_lnclient_hold_invoice_accepted":
871 lnClientTransaction, ok := event.Properties.(*lnclient.Transaction)
872 if !ok {
873 logger.Logger.WithField("event", event).Error("Failed to cast event properties for hold invoice accepted")
874 return
875 }
876 if lnClientTransaction.SettleDeadline == nil {
877 logger.Logger.WithField("event", event).Error("Transaction has no settle deadline")
878 return
879 }
880 svc.markHoldInvoiceAccepted(lnClientTransaction.Invoice, lnClientTransaction.PaymentHash, *lnClientTransaction.SettleDeadline, false)
881
882 case "nwc_lnclient_payment_sent":
883 lnClientTransaction, ok := event.Properties.(*lnclient.Transaction)
884 if !ok {
885 logger.Logger.WithField("event", event).Error("Failed to cast event")
886 return
887 }
888
889 var dbTransaction db.Transaction
890
891 // first lookup by pending
892 result := svc.db.Limit(1).Find(&dbTransaction, &db.Transaction{
893 Type: constants.TRANSACTION_TYPE_OUTGOING,
894 State: constants.TRANSACTION_STATE_PENDING,
895 PaymentHash: lnClientTransaction.PaymentHash,
896 })
897
898 if result.Error != nil {
899 logger.Logger.WithFields(logrus.Fields{
900 "payment_hash": lnClientTransaction.PaymentHash,
901 }).WithError(result.Error).Error("Failed to find transaction")
902 return
903 }
904
905 if result.RowsAffected == 0 {
906 // if no pending payment was found, lookup by failed, latest updated first
907 result := svc.db.Limit(1).Order("updated_at DESC").Find(&dbTransaction, &db.Transaction{
908 Type: constants.TRANSACTION_TYPE_OUTGOING,
909 State: constants.TRANSACTION_STATE_FAILED,
910 PaymentHash: lnClientTransaction.PaymentHash,
911 })
912
913 if result.Error != nil {
914 logger.Logger.WithFields(logrus.Fields{
915 "payment_hash": lnClientTransaction.PaymentHash,
916 }).WithError(result.Error).Error("Failed to find transaction")
917 return
918 }
919
920 if result.RowsAffected == 0 {
921 result := svc.db.Limit(1).Find(&dbTransaction, &db.Transaction{
922 Type: constants.TRANSACTION_TYPE_OUTGOING,
923 PaymentHash: lnClientTransaction.PaymentHash,
924 })
925
926 if result.Error != nil {
927 logger.Logger.WithFields(logrus.Fields{
928 "payment_hash": lnClientTransaction.PaymentHash,
929 }).WithError(result.Error).Error("Failed to find transaction")
930 return
931 }
932
933 if result.RowsAffected == 0 {
934 dbTransaction = db.Transaction{
935 Type: constants.TRANSACTION_TYPE_OUTGOING,
936 AmountMsat: uint64(lnClientTransaction.AmountMsat),
937 FeeReserveMsat: 0,
938 PaymentRequest: lnClientTransaction.Invoice,
939 PaymentHash: lnClientTransaction.PaymentHash,
940 Description: lnClientTransaction.Description,
941 DescriptionHash: lnClientTransaction.DescriptionHash,
942 }
943
944 if lnClientTransaction.ExpiresAt != nil {
945 expiresAtValue := time.Unix(*lnClientTransaction.ExpiresAt, 0)
946 dbTransaction.ExpiresAt = &expiresAtValue
947 }
948
949 if _, err := svc.createSettledTransactionFromNotification(&dbTransaction, lnClientTransaction.Preimage, uint64(lnClientTransaction.FeesPaidMsat), false); err != nil {
950 logger.Logger.WithFields(logrus.Fields{
951 "payment_hash": lnClientTransaction.PaymentHash,
952 }).WithError(err).Error("Failed to create settled transaction")
953 }
954 return
955 }
956 }
957 }
958
959 if _, err := svc.markTransactionSettled(&dbTransaction, lnClientTransaction.Preimage, uint64(lnClientTransaction.FeesPaidMsat), false); err != nil {
960 logger.Logger.WithFields(logrus.Fields{
961 "payment_hash": lnClientTransaction.PaymentHash,
962 }).WithError(err).Error("Failed to update transaction")
963 }
964 case "nwc_lnclient_payment_failed":
965 paymentFailedAsyncProperties, ok := event.Properties.(*lnclient.PaymentFailedEventProperties)
966 if !ok {
967 logger.Logger.WithField("event", event).Error("Failed to cast event")
968 return
969 }
970
971 lnClientTransaction := paymentFailedAsyncProperties.Transaction
972
973 var dbTransaction db.Transaction
974 result := svc.db.Limit(1).Find(&dbTransaction, &db.Transaction{
975 Type: constants.TRANSACTION_TYPE_OUTGOING,
976 State: constants.TRANSACTION_STATE_PENDING,
977 PaymentHash: lnClientTransaction.PaymentHash,
978 })
979
980 if result.RowsAffected == 0 {
981 logger.Logger.WithField("event", event).Error("Failed to find pending outgoing transaction by payment hash")
982 return
983 }
984
985 if _, err := svc.markPaymentFailed(&dbTransaction, paymentFailedAsyncProperties.Reason); err != nil {
986 logger.Logger.WithFields(logrus.Fields{
987 "payment_hash": lnClientTransaction.PaymentHash,
988 }).WithError(err).Error("Failed to mark payment as failed")
989 }
990 }
991 }
992
993 func (svc *transactionsService) markHoldInvoiceAccepted(paymentRequest string, paymentHash string, settleDeadline uint32, selfPayment bool) {
994 logger.Logger.WithFields(logrus.Fields{
995 "payment_request": paymentRequest,
996 "payment_hash": paymentHash,
997 "self_payment": selfPayment,
998 }).Info("Processing hold invoice accepted event")
999
1000 var dbTransaction db.Transaction
1001 err := svc.db.Transaction(func(tx *gorm.DB) error {
1002 // NOTE: filter by payment hash so the payment hash index is used,
1003 // but also match the payment request as wrapped invoices share the same hash
1004 result := tx.Where("payment_hash = ? AND payment_request = ? AND type = ? AND state = ?", paymentHash, paymentRequest, constants.TRANSACTION_TYPE_INCOMING, constants.TRANSACTION_STATE_PENDING).First(&dbTransaction)
1005 if result.Error != nil {
1006 if errors.Is(result.Error, gorm.ErrRecordNotFound) {
1007 logger.Logger.WithFields(logrus.Fields{
1008 "payment_request": paymentRequest,
1009 "payment_hash": paymentHash,
1010 }).Warn("No corresponding pending incoming transaction found in DB for accepted hold invoice")
1011 }
1012 logger.Logger.WithFields(logrus.Fields{
1013 "payment_request": paymentRequest,
1014 "payment_hash": paymentHash,
1015 }).WithError(result.Error).Error("Failed to query DB for accepted hold invoice")
1016 return result.Error
1017 }
1018
1019 err := tx.Model(&dbTransaction).UpdateColumns(map[string]interface{}{
1020 "state": constants.TRANSACTION_STATE_ACCEPTED,
1021 "self_payment": selfPayment,
1022 "settle_deadline": settleDeadline,
1023 }).Error
1024 if err != nil {
1025 logger.Logger.WithFields(logrus.Fields{
1026 "payment_request": paymentRequest,
1027 "payment_hash": paymentHash,
1028 "id": dbTransaction.ID,
1029 }).WithError(err).Error("Failed to update hold invoice state to accepted in DB")
1030 return err
1031 }
1032
1033 logger.Logger.WithFields(logrus.Fields{
1034 "payment_request": paymentRequest,
1035 "payment_hash": paymentHash,
1036 "id": dbTransaction.ID,
1037 }).Info("Updated hold invoice state to accepted in DB")
1038
1039 return nil
1040 })
1041 if err != nil {
1042 logger.Logger.WithFields(logrus.Fields{
1043 "payment_request": paymentRequest,
1044 "payment_hash": paymentHash,
1045 "id": dbTransaction.ID,
1046 }).WithError(err).Error("Failed DB transaction for hold invoice accepted event")
1047 } else {
1048 svc.eventPublisher.Publish(&events.Event{
1049 Event: "nwc_hold_invoice_accepted",
1050 Properties: &dbTransaction,
1051 })
1052 }
1053 }
1054
1055 func (svc *transactionsService) interceptSelfPayment(paymentRequest string, paymentHash string, lnClient lnclient.LNClient) (*lnclient.PayInvoiceResponse, error) {
1056 logger.Logger.WithFields(logrus.Fields{
1057 "payment_request": paymentRequest,
1058 "payment_hash": paymentHash,
1059 }).Debug("Intercepting self payment")
1060 incomingTransaction := db.Transaction{}
1061 result := svc.db.Limit(1).Find(&incomingTransaction, &db.Transaction{
1062 Type: constants.TRANSACTION_TYPE_INCOMING,
1063 State: constants.TRANSACTION_STATE_PENDING,
1064 // NOTE: for keysend, payment request will be ""
1065 PaymentRequest: paymentRequest,
1066 PaymentHash: paymentHash,
1067 })
1068 if result.Error != nil {
1069 return nil, result.Error
1070 }
1071
1072 if result.RowsAffected == 0 {
1073 return nil, NewNotFoundError()
1074 }
1075
1076 if incomingTransaction.Hold {
1077 return svc.interceptSelfHoldPayment(paymentRequest, paymentHash, lnClient)
1078 }
1079
1080 if incomingTransaction.Preimage == nil {
1081 return nil, errors.New("preimage is not set on transaction. Self payments not supported")
1082 }
1083
1084 _, err := svc.markTransactionSettled(&incomingTransaction, *incomingTransaction.Preimage, uint64(0), true)
1085 if err != nil {
1086 return nil, err
1087 }
1088
1089 return &lnclient.PayInvoiceResponse{
1090 Preimage: *incomingTransaction.Preimage,
1091 FeeMsat: 0,
1092 }, nil
1093 }
1094
1095 func (svc *transactionsService) interceptSelfHoldPayment(paymentRequest string, paymentHash string, lnClient lnclient.LNClient) (*lnclient.PayInvoiceResponse, error) {
1096 settledChannel := make(chan *db.Transaction)
1097 canceledChannel := make(chan *db.Transaction)
1098
1099 holdInvoiceUpdatedConsumer := newHoldInvoiceUpdatedConsumer(paymentRequest, settledChannel, canceledChannel)
1100
1101 svc.eventPublisher.RegisterSubscriber(holdInvoiceUpdatedConsumer)
1102 defer svc.eventPublisher.RemoveSubscriber(holdInvoiceUpdatedConsumer)
1103
1104 clientInfo, err := lnClient.GetInfo(context.Background())
1105 if err != nil {
1106 return nil, errors.New("failed to get client info")
1107 }
1108 if clientInfo.BlockHeight == 0 {
1109 return nil, errors.New("invalid client block height")
1110 }
1111
1112 fakeSettleDeadline := clientInfo.BlockHeight + 24
1113
1114 svc.markHoldInvoiceAccepted(paymentRequest, paymentHash, fakeSettleDeadline, true)
1115
1116 select {
1117 case settledTransaction := <-settledChannel:
1118 logger.Logger.WithField("settled_transaction", settledTransaction).Info("self hold payment was settled")
1119 if settledTransaction.Preimage == nil {
1120 return nil, errors.New("preimage is not set on self hold payment")
1121 }
1122
1123 return &lnclient.PayInvoiceResponse{
1124 Preimage: *settledTransaction.Preimage,
1125 FeeMsat: 0,
1126 }, nil
1127 case canceledTransaction := <-canceledChannel:
1128 logger.Logger.WithField("canceled_transaction", canceledTransaction).Info("self hold payment was canceled")
1129 return nil, lnclient.NewHoldInvoiceCanceledError()
1130 }
1131 }
1132
1133 func (svc *transactionsService) validateCanPay(tx *gorm.DB, appId *uint, amountMsat uint64, description string, selfPayment bool) error {
1134 amountWithFeeReserveMsat := amountMsat
1135 if !selfPayment {
1136 amountWithFeeReserveMsat += CalculateFeeReserveMsat(amountMsat)
1137 }
1138
1139 // ensure balance for isolated apps
1140 if appId != nil {
1141 var app db.App
1142 result := tx.Limit(1).Find(&app, &db.App{
1143 ID: *appId,
1144 })
1145 if result.RowsAffected == 0 {
1146 return NewNotFoundError()
1147 }
1148
1149 var appPermission db.AppPermission
1150 result = tx.Limit(1).Find(&appPermission, &db.AppPermission{
1151 AppId: *appId,
1152 Scope: constants.PAY_INVOICE_SCOPE,
1153 })
1154 if result.RowsAffected == 0 {
1155 return errors.New("app does not have pay_invoice scope")
1156 }
1157
1158 if app.Isolated {
1159 balanceMsat, err := queries.GetIsolatedBalanceMsat(tx, appPermission.AppId)
1160 if err != nil {
1161 return fmt.Errorf("failed to calculate isolated balance for app: %w", err)
1162 }
1163
1164 if int64(amountWithFeeReserveMsat) > balanceMsat {
1165 logger.Logger.WithFields(logrus.Fields{
1166 "balance_msat": balanceMsat,
1167 "self_payment": selfPayment,
1168 "amount_msat": amountMsat,
1169 "amount_with_fee_reserve_msat": amountWithFeeReserveMsat,
1170 }).Debug("Insufficient budget to make payment from isolated app")
1171 message := NewInsufficientBalanceError().Error()
1172 if description != "" {
1173 message += " " + description
1174 }
1175
1176 svc.eventPublisher.Publish(&events.Event{
1177 Event: "nwc_permission_denied",
1178 Properties: map[string]interface{}{
1179 "app_name": app.Name,
1180 "code": constants.ERROR_INSUFFICIENT_BALANCE,
1181 "message": message,
1182 },
1183 })
1184 return NewInsufficientBalanceError()
1185 }
1186 }
1187
1188 if appPermission.MaxAmountSat > 0 {
1189 budgetUsageMsat, err := queries.GetBudgetUsageMsat(tx, &appPermission)
1190 if err != nil {
1191 return fmt.Errorf("failed to calculate budget usage for app: %w", err)
1192 }
1193 if int(amountWithFeeReserveMsat/1000) > appPermission.MaxAmountSat-int(budgetUsageMsat/1000) {
1194 message := NewQuotaExceededError().Error()
1195 if description != "" {
1196 message += " " + description
1197 }
1198 svc.eventPublisher.Publish(&events.Event{
1199 Event: "nwc_permission_denied",
1200 Properties: map[string]interface{}{
1201 "app_name": app.Name,
1202 "code": constants.ERROR_QUOTA_EXCEEDED,
1203 "message": message,
1204 },
1205 })
1206 return NewQuotaExceededError()
1207 }
1208 }
1209 }
1210
1211 return nil
1212 }
1213
1214 // max of 1% or 10000 millisats (10 sats)
1215 func CalculateFeeReserveMsat(amountMsat uint64) uint64 {
1216 return uint64(math.Max(math.Ceil(float64(amountMsat)*0.01), 10000))
1217 }
1218
1219 func makePreimageHex() ([]byte, error) {
1220 bytes := make([]byte, 32) // 32 bytes * 8 bits/byte = 256 bits
1221 _, err := rand.Read(bytes)
1222 if err != nil {
1223 return nil, err
1224 }
1225 return bytes, nil
1226 }
1227
1228 func (svc *transactionsService) getBoostagramBytesFromCustomRecords(customRecords []lnclient.TLVRecord) []byte {
1229 for _, record := range customRecords {
1230 if record.Type == BoostagramTlvType {
1231 bytes, err := hex.DecodeString(record.Value)
1232 if err != nil {
1233 logger.Logger.WithField("value", record.Value).WithError(err).Error("failed to decode boostagram tlv hex value")
1234 return nil
1235 }
1236
1237 // ensure the boostagram is valid json
1238 var boostagram Boostagram
1239 if err := json.Unmarshal(bytes, &boostagram); err != nil {
1240 logger.Logger.WithField("value", string(bytes)).WithError(err).Error("failed to unmarshal boostagram to json")
1241 return nil
1242 }
1243
1244 return bytes
1245 }
1246 }
1247
1248 return nil
1249 }
1250
1251 func (svc *transactionsService) getDescriptionFromCustomRecords(customRecords []lnclient.TLVRecord) string {
1252 var description string
1253
1254 for _, record := range customRecords {
1255 switch record.Type {
1256 case BoostagramTlvType:
1257 bytes, err := hex.DecodeString(record.Value)
1258 if err != nil {
1259 continue
1260 }
1261 var boostagram Boostagram
1262 if err := json.Unmarshal(bytes, &boostagram); err != nil {
1263 continue
1264 }
1265 return boostagram.Message
1266
1267 // TODO: consider adding support for this in LDK
1268 case WhatsatTlvType:
1269 bytes, err := hex.DecodeString(record.Value)
1270 if err == nil {
1271 description = string(bytes)
1272 }
1273 }
1274 }
1275
1276 return description
1277 }
1278
1279 func (svc *transactionsService) getAppIdFromCustomRecords(customRecords []lnclient.TLVRecord, tx *gorm.DB) *uint {
1280 app := db.App{}
1281 for _, record := range customRecords {
1282 if record.Type == CustomKeyTlvType {
1283 decodedString, err := hex.DecodeString(record.Value)
1284 if err != nil {
1285 logger.Logger.WithError(err).Error("Failed to parse custom key TLV record as hex")
1286 continue
1287 }
1288 customValue, err := strconv.ParseUint(string(decodedString), 10, 64)
1289 if err != nil {
1290 logger.Logger.WithError(err).Error("Failed to parse custom key TLV record as number")
1291 continue
1292 }
1293 err = tx.Take(&app, &db.App{
1294 ID: uint(customValue),
1295 }).Error
1296 if err != nil {
1297 logger.Logger.WithError(err).Error("Failed to find app by id from custom key TLV record")
1298 continue
1299 }
1300 return &app.ID
1301 }
1302 }
1303 return nil
1304 }
1305
1306 func (svc *transactionsService) SettleHoldInvoice(ctx context.Context, preimage string, lnClient lnclient.LNClient) (*Transaction, error) {
1307 if len(preimage) != 64 {
1308 return nil, errors.New("invalid preimage format")
1309 }
1310 preimageBytes, err := hex.DecodeString(preimage)
1311 if err != nil {
1312 return nil, fmt.Errorf("invalid preimage hex: %w", err)
1313 }
1314
1315 paymentHashBytes := sha256.Sum256(preimageBytes)
1316 paymentHash := hex.EncodeToString(paymentHashBytes[:])
1317
1318 var dbTransaction db.Transaction
1319 result := svc.db.Limit(1).Find(&dbTransaction, &db.Transaction{
1320 Type: constants.TRANSACTION_TYPE_INCOMING,
1321 State: constants.TRANSACTION_STATE_ACCEPTED,
1322 PaymentHash: paymentHash,
1323 })
1324
1325 if result.RowsAffected == 0 {
1326 logger.Logger.WithField("payment_hash", paymentHash).Error("Failed to find accepted hold invoice")
1327 return nil, errors.New("failed to find accepted hold invoice")
1328 }
1329
1330 if !dbTransaction.SelfPayment {
1331 err = lnClient.SettleHoldInvoice(ctx, preimage)
1332 }
1333
1334 if err != nil {
1335 logger.Logger.WithFields(logrus.Fields{
1336 "preimage": preimage,
1337 }).WithError(err).Error("Failed to settle hold invoice via LN client")
1338 // Don't mark DB as failed here, as the settle might succeed later or might have already succeeded.
1339 return nil, err
1340 }
1341
1342 settledTransaction, err := svc.markTransactionSettled(&dbTransaction, preimage, 0, dbTransaction.SelfPayment)
1343 if err != nil {
1344 logger.Logger.WithFields(logrus.Fields{
1345 "payment_hash": paymentHash,
1346 "preimage": preimage,
1347 }).WithError(err).Error("Failed to mark hold invoice as settled")
1348 return nil, err
1349 }
1350
1351 return settledTransaction, nil
1352 }
1353
1354 func (svc *transactionsService) CancelHoldInvoice(ctx context.Context, paymentHash string, lnClient lnclient.LNClient) error {
1355
1356 var dbTransaction db.Transaction
1357 result := svc.db.Limit(1).Find(&dbTransaction, &db.Transaction{
1358 Type: constants.TRANSACTION_TYPE_INCOMING,
1359 State: constants.TRANSACTION_STATE_ACCEPTED,
1360 PaymentHash: paymentHash,
1361 })
1362
1363 if result.RowsAffected == 0 {
1364 logger.Logger.WithField("payment_hash", paymentHash).Error("Failed to find accepted hold invoice")
1365 return NewNotFoundError()
1366 }
1367
1368 if !dbTransaction.SelfPayment {
1369 err := lnClient.CancelHoldInvoice(ctx, paymentHash)
1370 if err != nil {
1371 logger.Logger.WithFields(logrus.Fields{
1372 "payment_hash": paymentHash,
1373 }).WithError(err).Error("Failed to cancel hold invoice via LN client")
1374 // Don't mark DB as failed here, cancellation might have already happened or might succeed later.
1375 return err
1376 }
1377 }
1378
1379 markedFailed, err := svc.markPaymentFailed(&dbTransaction, "Hold invoice was cancelled")
1380 if err != nil {
1381 logger.Logger.WithFields(logrus.Fields{
1382 "payment_hash": paymentHash,
1383 }).WithError(err).Error("Failed to mark hold invoice as failed due to cancellation")
1384 return err
1385 }
1386
1387 if !markedFailed {
1388 // a concurrent cancellation already marked the invoice as failed and
1389 // published the canceled event
1390 logger.Logger.WithFields(logrus.Fields{
1391 "payment_hash": paymentHash,
1392 }).Info("Hold invoice was already marked as failed")
1393 return nil
1394 }
1395
1396 logger.Logger.WithFields(logrus.Fields{
1397 "payment_hash": paymentHash,
1398 }).Info("Marked hold invoice as failed in DB due to cancellation")
1399
1400 svc.eventPublisher.Publish(&events.Event{
1401 Event: "nwc_hold_invoice_canceled",
1402 Properties: &dbTransaction,
1403 })
1404
1405 return nil
1406 }
1407
1408 func (svc *transactionsService) SetTransactionMetadata(ctx context.Context, id uint, metadata map[string]interface{}) error {
1409 var metadataBytes []byte
1410 metadataBytes, err := json.Marshal(metadata)
1411 if err != nil {
1412 logger.Logger.WithError(err).Error("Failed to serialize metadata")
1413 return err
1414 }
1415 if len(metadataBytes) > constants.INVOICE_METADATA_MAX_LENGTH {
1416 return fmt.Errorf("encoded invoice metadata provided is too large. Limit: %d Received: %d", constants.INVOICE_METADATA_MAX_LENGTH, len(metadataBytes))
1417 }
1418
1419 // UpdateColumn so we don't bump updated_at — metadata edits (e.g. user
1420 // labels) shouldn't reorder the transaction in the list.
1421 err = svc.db.Model(&db.Transaction{}).Where("id", id).UpdateColumn("metadata", datatypes.JSON(metadataBytes)).Error
1422 if err != nil {
1423 logger.Logger.WithError(err).WithField("metadata", metadata).Error("Failed to update transaction metadata")
1424 return err
1425 }
1426
1427 return nil
1428 }
1429
1430 func (svc *transactionsService) SetTransactionUserLabels(ctx context.Context, id uint, labels map[string]string) error {
1431 transaction := db.Transaction{}
1432 err := svc.db.WithContext(ctx).First(&transaction, id).Error
1433
1434 if err != nil {
1435 if errors.Is(err, gorm.ErrRecordNotFound) {
1436 return NewNotFoundError()
1437 }
1438 return err
1439 }
1440
1441 metadata := map[string]interface{}{}
1442 if transaction.Metadata != nil {
1443 if err := json.Unmarshal(transaction.Metadata, &metadata); err != nil {
1444 return fmt.Errorf("failed to decode existing metadata: %w", err)
1445 }
1446 }
1447
1448 sanitizedLabels := map[string]string{}
1449 for key, value := range labels {
1450 normalizedKey := strings.TrimSpace(key)
1451 normalizedValue := strings.TrimSpace(value)
1452 if normalizedKey == "" || normalizedValue == "" {
1453 continue
1454 }
1455 sanitizedLabels[normalizedKey] = normalizedValue
1456 }
1457
1458 if len(sanitizedLabels) == 0 {
1459 delete(metadata, "user_labels")
1460 } else {
1461 metadata["user_labels"] = sanitizedLabels
1462 }
1463
1464 return svc.SetTransactionMetadata(ctx, id, metadata)
1465 }
1466
1467 // markTransactionSettled marks an existing transaction as settled in its own
1468 // database transaction and publishes the corresponding events after it
1469 // commits, so subscribers never observe uncommitted state.
1470 func (svc *transactionsService) markTransactionSettled(dbTransaction *db.Transaction, preimage string, feeMsat uint64, selfPayment bool) (*db.Transaction, error) {
1471 if preimage == "" {
1472 return nil, errors.New("no preimage in payment")
1473 }
1474
1475 var settledTransaction *db.Transaction
1476 var eventsToPublish []*events.Event
1477 err := svc.db.Transaction(func(tx *gorm.DB) error {
1478 existingSettledTransaction, err := svc.findSettledTransaction(tx, dbTransaction)
1479 if err != nil {
1480 return err
1481 }
1482 if existingSettledTransaction != nil {
1483 logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).Debug("payment already marked as sent")
1484 settledTransaction = existingSettledTransaction
1485 return nil
1486 }
1487
1488 settledAt := time.Now()
1489 err = tx.Model(dbTransaction).Updates(map[string]interface{}{
1490 "State": constants.TRANSACTION_STATE_SETTLED,
1491 "Preimage": &preimage,
1492 "FeeMsat": feeMsat,
1493 "FeeReserveMsat": 0,
1494 "SettledAt": &settledAt,
1495 "SelfPayment": selfPayment,
1496 }).Error
1497 if err != nil {
1498 logger.Logger.WithFields(logrus.Fields{
1499 "payment_hash": dbTransaction.PaymentHash,
1500 }).WithError(err).Error("Failed to update DB transaction")
1501 return err
1502 }
1503
1504 logger.Logger.WithFields(logrus.Fields{
1505 "payment_hash": dbTransaction.PaymentHash,
1506 "type": dbTransaction.Type,
1507 }).Info("Marked transaction as settled")
1508
1509 settledTransaction = dbTransaction
1510 eventsToPublish = svc.afterTransactionSettled(tx, dbTransaction, &settledAt)
1511 return nil
1512 })
1513 if err != nil {
1514 return nil, err
1515 }
1516 svc.publishEvents(eventsToPublish)
1517
1518 return settledTransaction, nil
1519 }
1520
1521 // createSettledTransactionFromNotification inserts a transaction directly in
1522 // its settled state, in its own database transaction, and publishes the
1523 // corresponding events after it commits. It is for the case where the
1524 // LNClient notifies us of a sent or received payment we didn't already know
1525 // about (e.g. if the LNClient is an external node, and the payment was made
1526 // or received outside of Alby Hub, or a received keysend, which has no
1527 // invoice created upfront).
1528 func (svc *transactionsService) createSettledTransactionFromNotification(dbTransaction *db.Transaction, preimage string, feeMsat uint64, selfPayment bool) (*db.Transaction, error) {
1529 if preimage == "" {
1530 return nil, errors.New("no preimage in payment")
1531 }
1532
1533 var settledTransaction *db.Transaction
1534 var eventsToPublish []*events.Event
1535 err := svc.db.Transaction(func(tx *gorm.DB) error {
1536 existingSettledTransaction, err := svc.findSettledTransaction(tx, dbTransaction)
1537 if err != nil {
1538 return err
1539 }
1540 if existingSettledTransaction != nil {
1541 logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).Debug("payment already marked as settled")
1542 settledTransaction = existingSettledTransaction
1543 return nil
1544 }
1545
1546 settledAt := time.Now()
1547 dbTransaction.State = constants.TRANSACTION_STATE_SETTLED
1548 dbTransaction.Preimage = &preimage
1549 dbTransaction.FeeMsat = feeMsat
1550 dbTransaction.FeeReserveMsat = 0
1551 dbTransaction.SettledAt = &settledAt
1552 dbTransaction.SelfPayment = selfPayment
1553 if err := tx.Create(dbTransaction).Error; err != nil {
1554 logger.Logger.WithFields(logrus.Fields{
1555 "payment_hash": dbTransaction.PaymentHash,
1556 }).WithError(err).Error("Failed to create settled DB transaction")
1557 return err
1558 }
1559
1560 logger.Logger.WithFields(logrus.Fields{
1561 "payment_hash": dbTransaction.PaymentHash,
1562 "type": dbTransaction.Type,
1563 }).Info("Created settled transaction")
1564
1565 settledTransaction = dbTransaction
1566 eventsToPublish = svc.afterTransactionSettled(tx, dbTransaction, &settledAt)
1567 return nil
1568 })
1569 if err != nil {
1570 return nil, err
1571 }
1572 svc.publishEvents(eventsToPublish)
1573
1574 return settledTransaction, nil
1575 }
1576
1577 // lockTransactionsByPaymentHash takes a row lock on all transactions with the
1578 // given payment hash on postgres, so that concurrent state changes for the
1579 // same payment serialize (in sqlite transactions are serializable by default).
1580 func (svc *transactionsService) lockTransactionsByPaymentHash(tx *gorm.DB, paymentHash string) error {
1581 if tx.Dialector.Name() != "postgres" {
1582 return nil
1583 }
1584 transactionsWithPaymentHash := []db.Transaction{}
1585 err := tx.Where(&db.Transaction{
1586 PaymentHash: paymentHash,
1587 }).Clauses(clause.Locking{Strength: "UPDATE"}).Find(&transactionsWithPaymentHash).Error
1588 if err != nil {
1589 logger.Logger.WithField("payment_hash", paymentHash).WithError(err).Error("Failed to lock transactions by payment hash")
1590 }
1591 return err
1592 }
1593
1594 // findSettledTransaction returns the already-settled transaction matching
1595 // dbTransaction if one exists, locking all transactions with the same payment
1596 // hash to ensure only one transaction is settled per payment.
1597 func (svc *transactionsService) findSettledTransaction(tx *gorm.DB, dbTransaction *db.Transaction) (*db.Transaction, error) {
1598 if err := svc.lockTransactionsByPaymentHash(tx, dbTransaction.PaymentHash); err != nil {
1599 return nil, err
1600 }
1601
1602 var existingSettledTransaction db.Transaction
1603 result := tx.Limit(1).Find(&existingSettledTransaction, &db.Transaction{
1604 Type: dbTransaction.Type,
1605 PaymentRequest: dbTransaction.PaymentRequest,
1606 PaymentHash: dbTransaction.PaymentHash,
1607 State: constants.TRANSACTION_STATE_SETTLED,
1608 })
1609 if result.Error != nil {
1610 logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).WithError(result.Error).Error("Failed to check for existing settled transaction")
1611 return nil, result.Error
1612 }
1613 if result.RowsAffected > 0 {
1614 return &existingSettledTransaction, nil
1615 }
1616 return nil, nil
1617 }
1618
1619 // afterTransactionSettled runs the post-settlement side effects within the
1620 // caller's database transaction and returns the events to publish after it
1621 // commits.
1622 func (svc *transactionsService) afterTransactionSettled(tx *gorm.DB, dbTransaction *db.Transaction, settledAt *time.Time) []*events.Event {
1623 event := "nwc_payment_sent"
1624 if dbTransaction.Type == constants.TRANSACTION_TYPE_INCOMING {
1625 event = "nwc_payment_received"
1626 }
1627
1628 eventsToPublish := []*events.Event{{
1629 Event: event,
1630 Properties: dbTransaction,
1631 }}
1632
1633 if dbTransaction.AppId != nil {
1634 var app db.App
1635 result := tx.Limit(1).Find(&app, &db.App{
1636 ID: *dbTransaction.AppId,
1637 })
1638 if result.RowsAffected == 0 {
1639 logger.Logger.WithField("app_id", dbTransaction.AppId).Error("failed to find app by id")
1640 return eventsToPublish
1641 }
1642
1643 svc.updateAppLastSettledTransactionAt(&app, tx, settledAt)
1644
1645 if dbTransaction.Type == constants.TRANSACTION_TYPE_OUTGOING {
1646 if budgetWarningEvent := svc.checkBudgetUsage(&app, dbTransaction, tx); budgetWarningEvent != nil {
1647 eventsToPublish = append(eventsToPublish, budgetWarningEvent)
1648 }
1649 }
1650 }
1651
1652 return eventsToPublish
1653 }
1654
1655 func (svc *transactionsService) publishEvents(eventsToPublish []*events.Event) {
1656 for _, event := range eventsToPublish {
1657 svc.eventPublisher.Publish(event)
1658 }
1659 }
1660
1661 func (svc *transactionsService) updateAppLastSettledTransactionAt(app *db.App, gormTransaction *gorm.DB, settledAt *time.Time) {
1662 if err := gormTransaction.Model(app).Update("last_settled_transaction_at", settledAt).Error; err != nil {
1663 logger.Logger.WithField("app_id", app.ID).WithError(err).Error("failed to update app last settled transaction time")
1664 return
1665 }
1666 }
1667
1668 // checkBudgetUsage returns a budget warning event to publish after the
1669 // caller's database transaction commits, or nil if no warning is due.
1670 func (svc *transactionsService) checkBudgetUsage(app *db.App, dbTransaction *db.Transaction, gormTransaction *gorm.DB) *events.Event {
1671 if app.Isolated {
1672 return nil
1673 }
1674
1675 var appPermission db.AppPermission
1676 result := gormTransaction.Limit(1).Find(&appPermission, &db.AppPermission{
1677 AppId: app.ID,
1678 Scope: constants.PAY_INVOICE_SCOPE,
1679 })
1680 if result.RowsAffected == 0 {
1681 logger.Logger.WithField("app_id", dbTransaction.AppId).Error("failed to find pay_invoice scope")
1682 return nil
1683 }
1684
1685 budgetUsageMsat, err := queries.GetBudgetUsageMsat(gormTransaction, &appPermission)
1686 if err != nil {
1687 logger.Logger.WithField("app_id", dbTransaction.AppId).WithError(err).Error("failed to get budget usage")
1688 return nil
1689 }
1690 budgetUsageSat := budgetUsageMsat / 1000
1691 warningUsage := uint64(math.Floor(float64(appPermission.MaxAmountSat) * 0.8))
1692 if budgetUsageSat >= warningUsage && budgetUsageSat-dbTransaction.AmountMsat/1000 < warningUsage {
1693 return &events.Event{
1694 Event: "nwc_budget_warning",
1695 Properties: map[string]interface{}{
1696 "name": app.Name,
1697 "id": app.ID,
1698 },
1699 }
1700 }
1701 return nil
1702 }
1703
1704 // markPaymentFailed marks the transaction as failed in its own database
1705 // transaction and publishes the failed event after it commits, so subscribers
1706 // never observe uncommitted state. It returns whether this call transitioned
1707 // the transaction to failed (false if it was already failed), and refuses to
1708 // mark a settled transaction as failed.
1709 func (svc *transactionsService) markPaymentFailed(dbTransaction *db.Transaction, reason string) (bool, error) {
1710 markedFailed := false
1711 var eventsToPublish []*events.Event
1712 err := svc.db.Transaction(func(tx *gorm.DB) error {
1713 // lock all transactions with the same payment hash so a concurrent
1714 // settlement cannot slip in between the state check and the update
1715 if err := svc.lockTransactionsByPaymentHash(tx, dbTransaction.PaymentHash); err != nil {
1716 return err
1717 }
1718
1719 var existingTransaction db.Transaction
1720 result := tx.Limit(1).Find(&existingTransaction, &db.Transaction{
1721 ID: dbTransaction.ID,
1722 })
1723
1724 if result.Error != nil {
1725 logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).WithError(result.Error).Error("could not find transaction to mark as failed")
1726 return result.Error
1727 }
1728
1729 if result.RowsAffected == 0 {
1730 logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).Error("could not find transaction to mark as failed")
1731 return NewNotFoundError()
1732 }
1733
1734 if existingTransaction.State == constants.TRANSACTION_STATE_FAILED {
1735 logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).Info("payment already marked as failed")
1736 return nil
1737 }
1738
1739 if existingTransaction.State == constants.TRANSACTION_STATE_SETTLED {
1740 logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).Error("cannot mark settled payment as failed")
1741 return errors.New("cannot mark settled payment as failed")
1742 }
1743
1744 err := tx.Model(dbTransaction).Updates(map[string]interface{}{
1745 "State": constants.TRANSACTION_STATE_FAILED,
1746 "FeeReserveMsat": 0,
1747 "FailureReason": reason,
1748 }).Error
1749 if err != nil {
1750 logger.Logger.WithFields(logrus.Fields{
1751 "payment_hash": dbTransaction.PaymentHash,
1752 }).WithError(err).Error("Failed to mark transaction as failed")
1753 return err
1754 }
1755 logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).Info("Marked transaction as failed")
1756
1757 markedFailed = true
1758 eventsToPublish = append(eventsToPublish, &events.Event{
1759 Event: "nwc_payment_failed",
1760 Properties: dbTransaction,
1761 })
1762 return nil
1763 })
1764 if err != nil {
1765 return false, err
1766 }
1767 svc.publishEvents(eventsToPublish)
1768 return markedFailed, nil
1769 }
1770