From 12dbb48c8d844750a4f4f847a98d8f70424671e4 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Fri, 21 Jan 2022 20:57:41 +0100 Subject: [PATCH 1/8] First draft to subscribe for LND invoice update --- lib/service/invoices.go | 2 +- lib/service/invoicesubscription.go | 124 +++++++++++++++++++++++++++++ lib/service/service.go | 2 + main.go | 3 + 4 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 lib/service/invoicesubscription.go diff --git a/lib/service/invoices.go b/lib/service/invoices.go index 67a6d47..9f9f72d 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -268,7 +268,7 @@ func (svc *LndhubService) AddIncomingInvoice(userID int64, amount int64, memo, d invoice.Preimage = hex.EncodeToString(preimage) invoice.AddIndex = lnInvoiceResult.AddIndex invoice.DestinationPubkeyHex = svc.GetIdentPubKeyHex() // Our node pubkey for incoming invoices - invoice.State = "created" + invoice.State = "open" _, err = svc.DB.NewUpdate().Model(&invoice).WherePK().Exec(context.TODO()) if err != nil { diff --git a/lib/service/invoicesubscription.go b/lib/service/invoicesubscription.go new file mode 100644 index 0000000..9757d8d --- /dev/null +++ b/lib/service/invoicesubscription.go @@ -0,0 +1,124 @@ +package service + +import ( + "context" + "database/sql" + "encoding/hex" + "strings" + "time" + + "github.com/getAlby/lndhub.go/db/models" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/uptrace/bun" +) + +func (svc *LndhubService) InvoiceUpdateSubscription(ctx context.Context) error { + invoiceSubscriptionOptions := lnrpc.InvoiceSubscription{} + invoiceSubscriptionStream, err := svc.LndClient.SubscribeInvoices(context.Background(), &invoiceSubscriptionOptions) + if err != nil { + return err + } + svc.Logger.Info("Subscribed to invoice updates starting from index: ") + for { + // receive the next invoice update + rawInvoice, err := invoiceSubscriptionStream.Recv() + if err != nil { + // TODO: sentry notification + svc.Logger.Errorf("Error processing invoice update subscription: %v", err) + continue + } + var invoice models.Invoice + rHashStr := hex.EncodeToString(rawInvoice.RHash) + + svc.Logger.Infof("Invoice update: r_hash:%s state:%v", rHashStr, rawInvoice.State) + + // Ignore updates for open invoices + // We store the invoice details in the AddInvoice call + // This could cause a race condition here where we get this notification faster than we finish the AddInvoice call + if rawInvoice.State == lnrpc.Invoice_OPEN { + svc.Logger.Infof("Invoice state is open. Ignoring update. r_hash:%v", rHashStr) + continue + } + + // Search for the invoice in our DB + err = svc.DB.NewSelect().Model(&invoice).Where("type = ? AND r_hash = ? AND state <> ? ", "incoming", rHashStr, "settled").Limit(1).Scan(context.TODO()) + if err != nil { + // TODO: sentry notification + svc.Logger.Errorf("Could not find invoice: r_hash:%s payment_request:%s", rHashStr, rawInvoice.PaymentRequest) + continue + } + + // Update the DB entry of the invoice + // If the invoice is settled we save the settle date and the status otherwise we just store the lnd status + // + // Additionally to the invoice update we create a transaction entry from the incoming account to the user's current account + svc.Logger.Infof("Invoice update: invoice_id:%v settled:%v value:%v state:%v", invoice.ID, rawInvoice.Settled, rawInvoice.AmtPaidSat, rawInvoice.State) + + // Get the user's current and incoming account for the transaction entry + creditAccount, err := svc.AccountFor(ctx, "current", invoice.UserID) + if err != nil { + svc.Logger.Errorf("Could not find current account user_id:%v invoice_id:%v", invoice.UserID, invoice.ID) + // TODO: sentry notification + continue + } + debitAccount, err := svc.AccountFor(ctx, "incoming", invoice.UserID) + if err != nil { + svc.Logger.Errorf("Could not find incoming account user_id:%v invoice_id:%v", invoice.UserID, invoice.ID) + // TODO: sentry notification + continue + } + + tx, err := svc.DB.BeginTx(ctx, &sql.TxOptions{}) + if err != nil { + svc.Logger.Errorf("Failed to update the invoice invoice_id:%v r_hash:%s %v", invoice.ID, rHashStr, err) + // TODO: notify sentry + continue + } + + // if the invoice is NOT settled we just update the invoice state + if !rawInvoice.Settled { + svc.Logger.Infof("Invoice not settled invoice_id:%v", invoice.ID) + invoice.State = strings.ToLower(rawInvoice.State.String()) + + // if the invoice is settled we update the state and create an transaction entry to the current account + } else { + invoice.SettledAt = bun.NullTime{Time: time.Unix(rawInvoice.SettleDate, 0)} + invoice.State = "settled" + _, err = tx.NewUpdate().Model(&invoice).WherePK().Exec(context.TODO()) + if err != nil { + tx.Rollback() + svc.Logger.Errorf("Could not update invoice invoice_id:%v", invoice.ID) + // TODO: sentry notification + continue + } + + // Transfer the amount from the incoming account to the current account + entry := models.TransactionEntry{ + UserID: invoice.UserID, + InvoiceID: invoice.ID, + CreditAccountID: creditAccount.ID, + DebitAccountID: debitAccount.ID, + Amount: invoice.Amount, + } + // The DB constraints make sure the user actually has enough balance for the transaction + // If the user does not have enough balance this call fails + _, err = tx.NewInsert().Model(&entry).Exec(context.TODO()) + if err != nil { + tx.Rollback() + svc.Logger.Errorf("Could not create incoming->current transaction user_id:%v invoice_id:%v %v", invoice.UserID, invoice.ID, err) + // TODO: sentry notification + tx.Rollback() + continue + } + } + // Commit the DB transaction. Done, everything worked + err = tx.Commit() + if err != nil { + svc.Logger.Errorf("Failed to commit DB transaction user_id:%v invoice_id:%v %v", invoice.UserID, invoice.ID, err) + // TODO: sentry notification + continue + } + + } + return nil +} diff --git a/lib/service/service.go b/lib/service/service.go index c68cd1f..4af9a3d 100644 --- a/lib/service/service.go +++ b/lib/service/service.go @@ -11,6 +11,7 @@ import ( "github.com/labstack/gommon/random" "github.com/lightningnetwork/lnd/lnrpc" "github.com/uptrace/bun" + "github.com/ziflex/lecho/v3" "golang.org/x/crypto/bcrypt" ) @@ -20,6 +21,7 @@ type LndhubService struct { Config *Config DB *bun.DB LndClient lnrpc.LightningClient + Logger *lecho.Logger IdentityPubkey *btcec.PublicKey } diff --git a/main.go b/main.go index 962bd1c..04a6d2e 100644 --- a/main.go +++ b/main.go @@ -118,6 +118,7 @@ func main() { Config: c, DB: dbConn, LndClient: lndClient, + Logger: logger, IdentityPubkey: identityPubKey, } @@ -141,6 +142,8 @@ func main() { secured.GET("/getpending", blankController.GetPending) e.GET("/", blankController.Home) + go svc.InvoiceUpdateSubscription(context.Background()) + // Start server go func() { if err := e.Start(":3000"); err != nil && err != http.ErrServerClosed { From 71eabae4432b4669f4b3be103ddc13a71ccd76ba Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Fri, 21 Jan 2022 21:29:16 +0100 Subject: [PATCH 2/8] Split up process invoice updates --- lib/service/invoicesubscription.go | 174 +++++++++++++++-------------- 1 file changed, 89 insertions(+), 85 deletions(-) diff --git a/lib/service/invoicesubscription.go b/lib/service/invoicesubscription.go index 9757d8d..5e65a42 100644 --- a/lib/service/invoicesubscription.go +++ b/lib/service/invoicesubscription.go @@ -12,6 +12,87 @@ import ( "github.com/uptrace/bun" ) +func (svc *LndhubService) ProcessInvoiceUpdate(ctx context.Context, rawInvoice *lnrpc.Invoice) error { + var invoice models.Invoice + rHashStr := hex.EncodeToString(rawInvoice.RHash) + + svc.Logger.Infof("Invoice update: r_hash:%s state:%v", rHashStr, rawInvoice.State.String()) + + // Search for an incoming invoice with the r_hash that is NOT settled in our DB + err := svc.DB.NewSelect().Model(&invoice).Where("type = ? AND r_hash = ? AND state <> ? ", "incoming", rHashStr, "settled").Limit(1).Scan(context.TODO()) + if err != nil { + return err + } + + // Update the DB entry of the invoice + // If the invoice is settled we save the settle date and the status otherwise we just store the lnd status + // Additionally to the invoice update we create a transaction entry from the user's incoming account to the user's current account + // This transaction entry makes the balance available for the user + svc.Logger.Infof("Invoice update: invoice_id:%v settled:%v value:%v state:%v", invoice.ID, rawInvoice.Settled, rawInvoice.AmtPaidSat, rawInvoice.State) + + // Get the user's current account for the transaction entry + creditAccount, err := svc.AccountFor(ctx, "current", invoice.UserID) + if err != nil { + svc.Logger.Errorf("Could not find current account user_id:%v invoice_id:%v", invoice.UserID, invoice.ID) + return err + } + // Get the user's incoming account for the transaction entry + debitAccount, err := svc.AccountFor(ctx, "incoming", invoice.UserID) + if err != nil { + svc.Logger.Errorf("Could not find incoming account user_id:%v invoice_id:%v", invoice.UserID, invoice.ID) + return err + } + + // Process any update in a DB transaction + tx, err := svc.DB.BeginTx(ctx, &sql.TxOptions{}) + if err != nil { + svc.Logger.Errorf("Failed to update the invoice invoice_id:%v r_hash:%s %v", invoice.ID, rHashStr, err) + return err + } + + // if the invoice is NOT settled we just update the invoice state + if !rawInvoice.Settled { + svc.Logger.Infof("Invoice not settled invoice_id:%v state:", invoice.ID, rawInvoice.State.String()) + invoice.State = strings.ToLower(rawInvoice.State.String()) + + } else { + // if the invoice is settled we update the state and create an transaction entry to the current account + invoice.SettledAt = bun.NullTime{Time: time.Unix(rawInvoice.SettleDate, 0)} + invoice.State = "settled" + _, err = tx.NewUpdate().Model(&invoice).WherePK().Exec(context.TODO()) + if err != nil { + tx.Rollback() + svc.Logger.Errorf("Could not update invoice invoice_id:%v", invoice.ID) + return err + } + + // Transfer the amount from the user's incoming account to the user's current account + entry := models.TransactionEntry{ + UserID: invoice.UserID, + InvoiceID: invoice.ID, + CreditAccountID: creditAccount.ID, + DebitAccountID: debitAccount.ID, + Amount: invoice.Amount, + } + + // Save the transaction entry + _, err = tx.NewInsert().Model(&entry).Exec(context.TODO()) + if err != nil { + tx.Rollback() + svc.Logger.Errorf("Could not create incoming->current transaction user_id:%v invoice_id:%v %v", invoice.UserID, invoice.ID, err) + return err + } + } + // Commit the DB transaction. Done, everything worked + err = tx.Commit() + if err != nil { + svc.Logger.Errorf("Failed to commit DB transaction user_id:%v invoice_id:%v %v", invoice.UserID, invoice.ID, err) + return err + } + + return nil +} + func (svc *LndhubService) InvoiceUpdateSubscription(ctx context.Context) error { invoiceSubscriptionOptions := lnrpc.InvoiceSubscription{} invoiceSubscriptionStream, err := svc.LndClient.SubscribeInvoices(context.Background(), &invoiceSubscriptionOptions) @@ -19,6 +100,7 @@ func (svc *LndhubService) InvoiceUpdateSubscription(ctx context.Context) error { return err } svc.Logger.Info("Subscribed to invoice updates starting from index: ") + for { // receive the next invoice update rawInvoice, err := invoiceSubscriptionStream.Recv() @@ -27,98 +109,20 @@ func (svc *LndhubService) InvoiceUpdateSubscription(ctx context.Context) error { svc.Logger.Errorf("Error processing invoice update subscription: %v", err) continue } - var invoice models.Invoice - rHashStr := hex.EncodeToString(rawInvoice.RHash) - - svc.Logger.Infof("Invoice update: r_hash:%s state:%v", rHashStr, rawInvoice.State) // Ignore updates for open invoices // We store the invoice details in the AddInvoice call - // This could cause a race condition here where we get this notification faster than we finish the AddInvoice call + // Processing open invoices here could cause a race condition: + // We could get this notification faster than we finish the AddInvoice call if rawInvoice.State == lnrpc.Invoice_OPEN { - svc.Logger.Infof("Invoice state is open. Ignoring update. r_hash:%v", rHashStr) + svc.Logger.Infof("Invoice state is open. Ignoring update. r_hash:%v", hex.EncodeToString(rawInvoice.RHash)) continue } - // Search for the invoice in our DB - err = svc.DB.NewSelect().Model(&invoice).Where("type = ? AND r_hash = ? AND state <> ? ", "incoming", rHashStr, "settled").Limit(1).Scan(context.TODO()) - if err != nil { - // TODO: sentry notification - svc.Logger.Errorf("Could not find invoice: r_hash:%s payment_request:%s", rHashStr, rawInvoice.PaymentRequest) - continue + processingError := svc.ProcessInvoiceUpdate(ctx, rawInvoice) + if processingError != nil { + svc.Logger.Error(err) + // TODO sentry notification } - - // Update the DB entry of the invoice - // If the invoice is settled we save the settle date and the status otherwise we just store the lnd status - // - // Additionally to the invoice update we create a transaction entry from the incoming account to the user's current account - svc.Logger.Infof("Invoice update: invoice_id:%v settled:%v value:%v state:%v", invoice.ID, rawInvoice.Settled, rawInvoice.AmtPaidSat, rawInvoice.State) - - // Get the user's current and incoming account for the transaction entry - creditAccount, err := svc.AccountFor(ctx, "current", invoice.UserID) - if err != nil { - svc.Logger.Errorf("Could not find current account user_id:%v invoice_id:%v", invoice.UserID, invoice.ID) - // TODO: sentry notification - continue - } - debitAccount, err := svc.AccountFor(ctx, "incoming", invoice.UserID) - if err != nil { - svc.Logger.Errorf("Could not find incoming account user_id:%v invoice_id:%v", invoice.UserID, invoice.ID) - // TODO: sentry notification - continue - } - - tx, err := svc.DB.BeginTx(ctx, &sql.TxOptions{}) - if err != nil { - svc.Logger.Errorf("Failed to update the invoice invoice_id:%v r_hash:%s %v", invoice.ID, rHashStr, err) - // TODO: notify sentry - continue - } - - // if the invoice is NOT settled we just update the invoice state - if !rawInvoice.Settled { - svc.Logger.Infof("Invoice not settled invoice_id:%v", invoice.ID) - invoice.State = strings.ToLower(rawInvoice.State.String()) - - // if the invoice is settled we update the state and create an transaction entry to the current account - } else { - invoice.SettledAt = bun.NullTime{Time: time.Unix(rawInvoice.SettleDate, 0)} - invoice.State = "settled" - _, err = tx.NewUpdate().Model(&invoice).WherePK().Exec(context.TODO()) - if err != nil { - tx.Rollback() - svc.Logger.Errorf("Could not update invoice invoice_id:%v", invoice.ID) - // TODO: sentry notification - continue - } - - // Transfer the amount from the incoming account to the current account - entry := models.TransactionEntry{ - UserID: invoice.UserID, - InvoiceID: invoice.ID, - CreditAccountID: creditAccount.ID, - DebitAccountID: debitAccount.ID, - Amount: invoice.Amount, - } - // The DB constraints make sure the user actually has enough balance for the transaction - // If the user does not have enough balance this call fails - _, err = tx.NewInsert().Model(&entry).Exec(context.TODO()) - if err != nil { - tx.Rollback() - svc.Logger.Errorf("Could not create incoming->current transaction user_id:%v invoice_id:%v %v", invoice.UserID, invoice.ID, err) - // TODO: sentry notification - tx.Rollback() - continue - } - } - // Commit the DB transaction. Done, everything worked - err = tx.Commit() - if err != nil { - svc.Logger.Errorf("Failed to commit DB transaction user_id:%v invoice_id:%v %v", invoice.UserID, invoice.ID, err) - // TODO: sentry notification - continue - } - } - return nil } From c75e703d80f34f07236c9cc60bd9235668f95270 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Sat, 22 Jan 2022 12:57:00 +0100 Subject: [PATCH 3/8] Try reconnecting on invoice subscription errors --- lib/service/invoicesubscription.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/service/invoicesubscription.go b/lib/service/invoicesubscription.go index 5e65a42..551be4b 100644 --- a/lib/service/invoicesubscription.go +++ b/lib/service/invoicesubscription.go @@ -93,20 +93,28 @@ func (svc *LndhubService) ProcessInvoiceUpdate(ctx context.Context, rawInvoice * return nil } -func (svc *LndhubService) InvoiceUpdateSubscription(ctx context.Context) error { +func (svc *LndhubService) ConnectInvoiceSubscription(ctx context.Context) (lnrpc.Lightning_SubscribeInvoicesClient, error) { invoiceSubscriptionOptions := lnrpc.InvoiceSubscription{} - invoiceSubscriptionStream, err := svc.LndClient.SubscribeInvoices(context.Background(), &invoiceSubscriptionOptions) + return svc.LndClient.SubscribeInvoices(ctx, &invoiceSubscriptionOptions) +} + +func (svc *LndhubService) InvoiceUpdateSubscription(ctx context.Context) error { + invoiceSubscriptionStream, err := svc.ConnectInvoiceSubscription(ctx) if err != nil { return err } svc.Logger.Info("Subscribed to invoice updates starting from index: ") - for { // receive the next invoice update rawInvoice, err := invoiceSubscriptionStream.Recv() if err != nil { // TODO: sentry notification svc.Logger.Errorf("Error processing invoice update subscription: %v", err) + // TODO: close the stream somehoe before retrying? + // Wait 30 seconds and try to reconnect + // TODO: implement some backoff + time.Sleep(30 * time.Second) + invoiceSubscriptionStream, _ = svc.ConnectInvoiceSubscription(ctx) continue } From b54f04701fe50f8e0001be76d27a6a849174c448 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Sat, 22 Jan 2022 13:24:51 +0100 Subject: [PATCH 4/8] Start invoice subscription from the last unsettled invoice We want to get updates starting from those invoices. This allows us to catch up in case the app was offline while some invoices got settled --- lib/service/invoicesubscription.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/service/invoicesubscription.go b/lib/service/invoicesubscription.go index 551be4b..49f0a55 100644 --- a/lib/service/invoicesubscription.go +++ b/lib/service/invoicesubscription.go @@ -94,7 +94,15 @@ func (svc *LndhubService) ProcessInvoiceUpdate(ctx context.Context, rawInvoice * } func (svc *LndhubService) ConnectInvoiceSubscription(ctx context.Context) (lnrpc.Lightning_SubscribeInvoicesClient, error) { + var invoice models.Invoice invoiceSubscriptionOptions := lnrpc.InvoiceSubscription{} + // Find the oldest NOT settled invoice with an add_index + err := svc.DB.NewSelect().Model(&invoice).Where("invoice.settled_at IS NULL AND invoice.add_index IS NOT NULL").OrderExpr("invoice.id ASC").Limit(1).Scan(ctx) + // IF we found an invoice we use that index to start the subscription + if err == nil { + invoiceSubscriptionOptions = lnrpc.InvoiceSubscription{AddIndex: invoice.AddIndex - 1} // -1 because we want updates for that invoice already + } + svc.Logger.Infof("Starting invoice subscription from index: %v", invoiceSubscriptionOptions.AddIndex) return svc.LndClient.SubscribeInvoices(ctx, &invoiceSubscriptionOptions) } @@ -103,7 +111,6 @@ func (svc *LndhubService) InvoiceUpdateSubscription(ctx context.Context) error { if err != nil { return err } - svc.Logger.Info("Subscribed to invoice updates starting from index: ") for { // receive the next invoice update rawInvoice, err := invoiceSubscriptionStream.Recv() From a4e597ccfc697b407a03a840ae013af8519ebb83 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Sat, 22 Jan 2022 13:30:13 +0100 Subject: [PATCH 5/8] Comments --- main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/main.go b/main.go index 04a6d2e..f6754c5 100644 --- a/main.go +++ b/main.go @@ -142,6 +142,7 @@ func main() { secured.GET("/getpending", blankController.GetPending) e.GET("/", blankController.Home) + // Subscribe to LND invoice updates in the background go svc.InvoiceUpdateSubscription(context.Background()) // Start server From d90a6951723ce14239f82b7efd920bf69afe2607 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Sat, 22 Jan 2022 13:32:15 +0100 Subject: [PATCH 6/8] Context how do they work... --- lib/service/invoicesubscription.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/service/invoicesubscription.go b/lib/service/invoicesubscription.go index 49f0a55..f04ccfc 100644 --- a/lib/service/invoicesubscription.go +++ b/lib/service/invoicesubscription.go @@ -19,7 +19,7 @@ func (svc *LndhubService) ProcessInvoiceUpdate(ctx context.Context, rawInvoice * svc.Logger.Infof("Invoice update: r_hash:%s state:%v", rHashStr, rawInvoice.State.String()) // Search for an incoming invoice with the r_hash that is NOT settled in our DB - err := svc.DB.NewSelect().Model(&invoice).Where("type = ? AND r_hash = ? AND state <> ? ", "incoming", rHashStr, "settled").Limit(1).Scan(context.TODO()) + err := svc.DB.NewSelect().Model(&invoice).Where("type = ? AND r_hash = ? AND state <> ? ", "incoming", rHashStr, "settled").Limit(1).Scan(ctx) if err != nil { return err } @@ -59,7 +59,7 @@ func (svc *LndhubService) ProcessInvoiceUpdate(ctx context.Context, rawInvoice * // if the invoice is settled we update the state and create an transaction entry to the current account invoice.SettledAt = bun.NullTime{Time: time.Unix(rawInvoice.SettleDate, 0)} invoice.State = "settled" - _, err = tx.NewUpdate().Model(&invoice).WherePK().Exec(context.TODO()) + _, err = tx.NewUpdate().Model(&invoice).WherePK().Exec(ctx) if err != nil { tx.Rollback() svc.Logger.Errorf("Could not update invoice invoice_id:%v", invoice.ID) @@ -76,7 +76,7 @@ func (svc *LndhubService) ProcessInvoiceUpdate(ctx context.Context, rawInvoice * } // Save the transaction entry - _, err = tx.NewInsert().Model(&entry).Exec(context.TODO()) + _, err = tx.NewInsert().Model(&entry).Exec(ctx) if err != nil { tx.Rollback() svc.Logger.Errorf("Could not create incoming->current transaction user_id:%v invoice_id:%v %v", invoice.UserID, invoice.ID, err) From af8c72d2c92e96b5cf51efa26869515253b90119 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Sat, 22 Jan 2022 21:11:19 +0100 Subject: [PATCH 7/8] Store invoice expiry and use search for not expired invoices in the invoice subscription When subscribing for invoice updates we only subscribe to not expired invoices. We assume expired invoices will never get paid. --- db/models/invoice.go | 1 + lib/service/invoices.go | 6 +++++- lib/service/invoicesubscription.go | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/db/models/invoice.go b/db/models/invoice.go index 17b705c..0456d5a 100644 --- a/db/models/invoice.go +++ b/db/models/invoice.go @@ -26,6 +26,7 @@ type Invoice struct { State string `json:"state" bun:",default:'initialized'"` AddIndex uint64 `json:"add_index" bun:",nullzero"` CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` + ExpiresAt bun.NullTime `bun:",nullzero"` UpdatedAt bun.NullTime `json:"updated_at"` SettledAt bun.NullTime `json:"settled_at"` } diff --git a/lib/service/invoices.go b/lib/service/invoices.go index 9f9f72d..382a602 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -197,6 +197,7 @@ func (svc *LndhubService) PayInvoice(invoice *models.Invoice) (*models.Transacti func (svc *LndhubService) AddOutgoingInvoice(userID int64, paymentRequest string, decodedInvoice zpay32.Invoice) (*models.Invoice, error) { // Initialize new DB invoice destinationPubkeyHex := hex.EncodeToString(decodedInvoice.Destination.SerializeCompressed()) + expiresAt := decodedInvoice.Timestamp.Add(decodedInvoice.Expiry()) invoice := models.Invoice{ Type: "outgoing", UserID: userID, @@ -204,6 +205,7 @@ func (svc *LndhubService) AddOutgoingInvoice(userID int64, paymentRequest string PaymentRequest: paymentRequest, State: "initialized", DestinationPubkeyHex: destinationPubkeyHex, + ExpiresAt: bun.NullTime{Time: expiresAt}, } if decodedInvoice.DescriptionHash != nil { dh := *decodedInvoice.DescriptionHash @@ -228,6 +230,7 @@ func (svc *LndhubService) AddOutgoingInvoice(userID int64, paymentRequest string func (svc *LndhubService) AddIncomingInvoice(userID int64, amount int64, memo, descriptionHashStr string) (*models.Invoice, error) { preimage := makePreimageHex() + expiry := time.Hour * 24 // invoice expires in 24h // Initialize new DB invoice invoice := models.Invoice{ Type: "incoming", @@ -236,6 +239,7 @@ func (svc *LndhubService) AddIncomingInvoice(userID int64, amount int64, memo, d Memo: memo, DescriptionHash: descriptionHashStr, State: "initialized", + ExpiresAt: bun.NullTime{Time: time.Now().Add(expiry)}, } // Save invoice - we save the invoice early to have a record in case the LN call fails @@ -254,7 +258,7 @@ func (svc *LndhubService) AddIncomingInvoice(userID int64, amount int64, memo, d DescriptionHash: descriptionHash, Value: amount, RPreimage: preimage, - Expiry: 3600 * 24, // 24h // TODO: move to config + Expiry: int64(expiry.Seconds()), } // Call LND lnInvoiceResult, err := svc.LndClient.AddInvoice(context.TODO(), &lnInvoice) diff --git a/lib/service/invoicesubscription.go b/lib/service/invoicesubscription.go index f04ccfc..20be2cc 100644 --- a/lib/service/invoicesubscription.go +++ b/lib/service/invoicesubscription.go @@ -19,7 +19,7 @@ func (svc *LndhubService) ProcessInvoiceUpdate(ctx context.Context, rawInvoice * svc.Logger.Infof("Invoice update: r_hash:%s state:%v", rHashStr, rawInvoice.State.String()) // Search for an incoming invoice with the r_hash that is NOT settled in our DB - err := svc.DB.NewSelect().Model(&invoice).Where("type = ? AND r_hash = ? AND state <> ? ", "incoming", rHashStr, "settled").Limit(1).Scan(ctx) + err := svc.DB.NewSelect().Model(&invoice).Where("type = ? AND r_hash = ? AND state <> ? AND expires_at > NOW()", "incoming", rHashStr, "settled").Limit(1).Scan(ctx) if err != nil { return err } From 35e7da2f3074af808e1ccc9e9d96ca321357bae1 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Sat, 22 Jan 2022 22:57:14 +0100 Subject: [PATCH 8/8] Search for correct state --- lib/service/invoices.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/service/invoices.go b/lib/service/invoices.go index 382a602..4572429 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -37,7 +37,7 @@ func (svc *LndhubService) SendInternalPayment(tx *bun.Tx, invoice *models.Invoic //SendInternalPayment() // find invoice var incomingInvoice models.Invoice - err := svc.DB.NewSelect().Model(&incomingInvoice).Where("type = ? AND payment_request = ? AND state = ? ", "incoming", invoice.PaymentRequest, "created").Limit(1).Scan(context.TODO()) + err := svc.DB.NewSelect().Model(&incomingInvoice).Where("type = ? AND payment_request = ? AND state = ? ", "incoming", invoice.PaymentRequest, "open").Limit(1).Scan(context.TODO()) if err != nil { // invoice not found or already settled // TODO: logging