diff --git a/.gitignore b/.gitignore index 477faf08..cbf20ef1 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ vendor/ # env, use .env_example for reference .env + +initialized_payments_dryrun/* \ No newline at end of file diff --git a/integration_tests/hodl_invoice_test.go b/integration_tests/hodl_invoice_test.go new file mode 100644 index 00000000..c731dd78 --- /dev/null +++ b/integration_tests/hodl_invoice_test.go @@ -0,0 +1,220 @@ +package integration_tests + +import ( + "context" + "encoding/hex" + "fmt" + "log" + "testing" + "time" + + "github.com/getAlby/lndhub.go/common" + "github.com/getAlby/lndhub.go/controllers" + "github.com/getAlby/lndhub.go/lib" + "github.com/getAlby/lndhub.go/lib/responses" + "github.com/getAlby/lndhub.go/lib/service" + "github.com/getAlby/lndhub.go/lib/tokens" + "github.com/go-playground/validator/v10" + "github.com/labstack/echo/v4" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type HodlInvoiceSuite struct { + TestSuite + mlnd *MockLND + externalLND *MockLND + service *service.LndhubService + userLogin ExpectedCreateUserResponseBody + userToken string + invoiceUpdateSubCancelFn context.CancelFunc + hodlLND *LNDMockHodlWrapperAsync +} + +func (suite *HodlInvoiceSuite) SetupSuite() { + mlnd := newDefaultMockLND() + externalLND, err := NewMockLND("1234567890abcdefabcd", 0, make(chan (*lnrpc.Invoice))) + if err != nil { + log.Fatalf("Error initializing test service: %v", err) + } + suite.externalLND = externalLND + suite.mlnd = mlnd + // inject hodl lnd client + lndClient, err := NewLNDMockHodlWrapperAsync(mlnd) + suite.hodlLND = lndClient + if err != nil { + log.Fatalf("Error setting up test client: %v", err) + } + + svc, err := LndHubTestServiceInit(lndClient) + if err != nil { + log.Fatalf("Error initializing test service: %v", err) + } + users, userTokens, err := createUsers(svc, 1) + if err != nil { + log.Fatalf("Error creating test users: %v", err) + } + // Subscribe to LND invoice updates in the background + // store cancel func to be called in tear down suite + ctx, cancel := context.WithCancel(context.Background()) + suite.invoiceUpdateSubCancelFn = cancel + go svc.InvoiceUpdateSubscription(ctx) + suite.service = svc + e := echo.New() + + e.HTTPErrorHandler = responses.HTTPErrorHandler + e.Validator = &lib.CustomValidator{Validator: validator.New()} + suite.echo = e + assert.Equal(suite.T(), 1, len(users)) + assert.Equal(suite.T(), 1, len(userTokens)) + suite.userLogin = users[0] + suite.userToken = userTokens[0] + suite.echo.Use(tokens.Middleware([]byte(suite.service.Config.JWTSecret))) + suite.echo.GET("/balance", controllers.NewBalanceController(suite.service).Balance) + suite.echo.POST("/addinvoice", controllers.NewAddInvoiceController(suite.service).AddInvoice) + suite.echo.POST("/payinvoice", controllers.NewPayInvoiceController(suite.service).PayInvoice) +} + +func (suite *HodlInvoiceSuite) TestHodlInvoice() { + userFundingSats := 1000 + externalSatRequested := 500 + // fund user account + invoiceResponse := suite.createAddInvoiceReq(userFundingSats, "integration test external payment user", suite.userToken) + err := suite.mlnd.mockPaidInvoice(invoiceResponse, 0, false, nil) + assert.NoError(suite.T(), err) + + // wait a bit for the callback event to hit + time.Sleep(10 * time.Millisecond) + + // create external invoice + externalInvoice := lnrpc.Invoice{ + Memo: "integration tests: external pay from user", + Value: int64(externalSatRequested), + RPreimage: []byte("preimage1"), + } + invoice, err := suite.externalLND.AddInvoice(context.Background(), &externalInvoice) + assert.NoError(suite.T(), err) + // pay external from user, req will be canceled after 2 sec + go suite.createPayInvoiceReqWithCancel(invoice.PaymentRequest, suite.userToken) + + // wait for payment to be updated as pending in database + time.Sleep(5 * time.Second) + + // check to see that balance was reduced + userId := getUserIdFromToken(suite.userToken) + userBalance, err := suite.service.CurrentUserBalance(context.Background(), userId) + if err != nil { + fmt.Printf("Error when getting balance %v\n", err.Error()) + } + assert.Equal(suite.T(), int64(userFundingSats-externalSatRequested), userBalance) + + // check payment is pending + inv, err := suite.service.FindInvoiceByPaymentHash(context.Background(), userId, hex.EncodeToString(invoice.RHash)) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), common.InvoiceStateInitialized, inv.State) + + //start payment checking loop + err = suite.service.CheckAllPendingOutgoingPayments(context.Background()) + assert.NoError(suite.T(), err) + //send cancel invoice with lnrpc.payment + suite.hodlLND.SettlePayment(lnrpc.Payment{ + PaymentHash: hex.EncodeToString(invoice.RHash), + Value: externalInvoice.Value, + CreationDate: 0, + Fee: 0, + PaymentPreimage: "", + ValueSat: externalInvoice.Value, + ValueMsat: 0, + PaymentRequest: invoice.PaymentRequest, + Status: lnrpc.Payment_FAILED, + FailureReason: lnrpc.PaymentFailureReason_FAILURE_REASON_INCORRECT_PAYMENT_DETAILS, + }) + //wait a bit for db update to happen + time.Sleep(time.Second) + + // check that balance was reverted and invoice is in error state + userBalance, err = suite.service.CurrentUserBalance(context.Background(), userId) + if err != nil { + fmt.Printf("Error when getting balance %v\n", err.Error()) + } + assert.Equal(suite.T(), int64(userFundingSats), userBalance) + + invoices, err := suite.service.InvoicesFor(context.Background(), userId, common.InvoiceTypeOutgoing) + if err != nil { + fmt.Printf("Error when getting invoices %v\n", err.Error()) + } + assert.Equal(suite.T(), 1, len(invoices)) + assert.Equal(suite.T(), common.InvoiceStateError, invoices[0].State) + errorString := "FAILURE_REASON_INCORRECT_PAYMENT_DETAILS" + assert.Equal(suite.T(), errorString, invoices[0].ErrorMessage) + + transactonEntries, err := suite.service.TransactionEntriesFor(context.Background(), userId) + if err != nil { + fmt.Printf("Error when getting transaction entries %v\n", err.Error()) + } + // check if there are 3 transaction entries, with reversed credit and debit account ids + assert.Equal(suite.T(), 3, len(transactonEntries)) + assert.Equal(suite.T(), transactonEntries[1].CreditAccountID, transactonEntries[2].DebitAccountID) + assert.Equal(suite.T(), transactonEntries[1].DebitAccountID, transactonEntries[2].CreditAccountID) + assert.Equal(suite.T(), transactonEntries[1].Amount, int64(externalSatRequested)) + assert.Equal(suite.T(), transactonEntries[2].Amount, int64(externalSatRequested)) + + // create external invoice + externalInvoice = lnrpc.Invoice{ + Memo: "integration tests: external pay from user", + Value: int64(externalSatRequested), + RPreimage: []byte("preimage2"), + } + invoice, err = suite.externalLND.AddInvoice(context.Background(), &externalInvoice) + assert.NoError(suite.T(), err) + // pay external from user, req will be canceled after 2 sec + go suite.createPayInvoiceReqWithCancel(invoice.PaymentRequest, suite.userToken) + // wait for payment to be updated as pending in database + time.Sleep(3 * time.Second) + // check payment is pending + inv, err = suite.service.FindInvoiceByPaymentHash(context.Background(), userId, hex.EncodeToString(invoice.RHash)) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), common.InvoiceStateInitialized, inv.State) + //start payment checking loop + err = suite.service.CheckAllPendingOutgoingPayments(context.Background()) + assert.NoError(suite.T(), err) + //send settle invoice with lnrpc.payment + suite.hodlLND.SettlePayment(lnrpc.Payment{ + PaymentHash: hex.EncodeToString(invoice.RHash), + Value: externalInvoice.Value, + CreationDate: 0, + Fee: 0, + PaymentPreimage: "preimage2", + ValueSat: externalInvoice.Value, + ValueMsat: 0, + PaymentRequest: invoice.PaymentRequest, + Status: lnrpc.Payment_SUCCEEDED, + FailureReason: 0, + }) + //wait a bit for db update to happen + time.Sleep(time.Second) + + if err != nil { + fmt.Printf("Error when getting balance %v\n", err.Error()) + } + //fetch user balance again + userBalance, err = suite.service.CurrentUserBalance(context.Background(), userId) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(userFundingSats-externalSatRequested), userBalance) + // check payment is updated as succesful + inv, err = suite.service.FindInvoiceByPaymentHash(context.Background(), userId, hex.EncodeToString(invoice.RHash)) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), common.InvoiceStateSettled, inv.State) + clearTable(suite.service, "invoices") + clearTable(suite.service, "transaction_entries") + clearTable(suite.service, "accounts") +} + +func (suite *HodlInvoiceSuite) TearDownSuite() { + suite.invoiceUpdateSubCancelFn() +} + +func TestHodlInvoiceSuite(t *testing.T) { + suite.Run(t, new(HodlInvoiceSuite)) +} diff --git a/integration_tests/lnd_mock.go b/integration_tests/lnd_mock.go index 77327ac5..9f01a35c 100644 --- a/integration_tests/lnd_mock.go +++ b/integration_tests/lnd_mock.go @@ -15,6 +15,7 @@ import ( "github.com/getAlby/lndhub.go/lnd" "github.com/labstack/gommon/random" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/zpay32" "google.golang.org/grpc" @@ -58,6 +59,9 @@ func (mockSub *MockSubscribeInvoices) Recv() (*lnrpc.Invoice, error) { inv := <-mockSub.invoiceChan return inv, nil } +func (mlnd *MockLND) SubscribePayment(ctx context.Context, req *routerrpc.TrackPaymentRequest, options ...grpc.CallOption) (lnd.SubscribePaymentWrapper, error) { + return nil, nil +} func (mlnd *MockLND) ListChannels(ctx context.Context, req *lnrpc.ListChannelsRequest, options ...grpc.CallOption) (*lnrpc.ListChannelsResponse, error) { return &lnrpc.ListChannelsResponse{ @@ -216,6 +220,10 @@ func (mlnd *MockLND) GetInfo(ctx context.Context, req *lnrpc.GetInfoRequest, opt }, nil } +func (mlnd *MockLND) TrackPayment(ctx context.Context, hash []byte, options ...grpc.CallOption) (*lnrpc.Payment, error) { + return nil, nil +} + func (mlnd *MockLND) DecodeBolt11(ctx context.Context, bolt11 string, options ...grpc.CallOption) (*lnrpc.PayReq, error) { inv, err := zpay32.Decode(bolt11, &chaincfg.RegressionNetParams) if err != nil { @@ -223,7 +231,7 @@ func (mlnd *MockLND) DecodeBolt11(ctx context.Context, bolt11 string, options .. } result := &lnrpc.PayReq{ Destination: hex.EncodeToString(inv.Destination.SerializeCompressed()), - PaymentHash: string(inv.PaymentHash[:]), + PaymentHash: hex.EncodeToString(inv.PaymentHash[:]), NumSatoshis: int64(*inv.MilliSat) / 1000, Timestamp: inv.Timestamp.Unix(), Expiry: int64(inv.Expiry()), diff --git a/integration_tests/lnd_mock_hodl.go b/integration_tests/lnd_mock_hodl.go new file mode 100644 index 00000000..01f7c5a3 --- /dev/null +++ b/integration_tests/lnd_mock_hodl.go @@ -0,0 +1,58 @@ +package integration_tests + +import ( + "context" + + "github.com/getAlby/lndhub.go/lnd" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "google.golang.org/grpc" +) + +func NewLNDMockHodlWrapper(lnd lnd.LightningClientWrapper) (result *LNDMockWrapper, err error) { + return &LNDMockWrapper{ + lnd, + }, nil +} + +type LNDMockHodlWrapperAsync struct { + hps *HodlPaymentSubscriber + lnd.LightningClientWrapper +} + +type HodlPaymentSubscriber struct { + ch chan (lnrpc.Payment) +} + +// wait for channel, then return +func (hps *HodlPaymentSubscriber) Recv() (*lnrpc.Payment, error) { + result := <-hps.ch + return &result, nil +} + +func NewLNDMockHodlWrapperAsync(lnd lnd.LightningClientWrapper) (result *LNDMockHodlWrapperAsync, err error) { + return &LNDMockHodlWrapperAsync{ + hps: &HodlPaymentSubscriber{ + ch: make(chan lnrpc.Payment), + }, + LightningClientWrapper: lnd, + }, nil +} + +func (wrapper *LNDMockHodlWrapperAsync) SubscribePayment(ctx context.Context, req *routerrpc.TrackPaymentRequest, options ...grpc.CallOption) (lnd.SubscribePaymentWrapper, error) { + return wrapper.hps, nil +} + +func (wrapper *LNDMockHodlWrapperAsync) SendPaymentSync(ctx context.Context, req *lnrpc.SendRequest, options ...grpc.CallOption) (*lnrpc.SendResponse, error) { + //block indefinetely + //because we don't want this function to ever return something here + //the payments should be processed asynchronously by the payment tracker + select {} +} + +func (wrapper *LNDMockHodlWrapperAsync) SettlePayment(payment lnrpc.Payment) { + wrapper.hps.ch <- payment +} + +//write test that completes payment +//write test that fails payment diff --git a/integration_tests/subscription_start_test.go b/integration_tests/subscription_start_test.go index 2cde79f4..25d3e6ec 100644 --- a/integration_tests/subscription_start_test.go +++ b/integration_tests/subscription_start_test.go @@ -17,6 +17,7 @@ import ( "github.com/go-playground/validator/v10" "github.com/labstack/echo/v4" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "github.com/uptrace/bun" @@ -128,6 +129,9 @@ func (mock *lndSubscriptionStartMockClient) SubscribeInvoices(ctx context.Contex func (mock *lndSubscriptionStartMockClient) Recv() (*lnrpc.Invoice, error) { select {} } +func (mock *lndSubscriptionStartMockClient) SubscribePayment(ctx context.Context, req *routerrpc.TrackPaymentRequest, options ...grpc.CallOption) (lnd.SubscribePaymentWrapper, error) { + return nil, nil +} func (mock *lndSubscriptionStartMockClient) GetInfo(ctx context.Context, req *lnrpc.GetInfoRequest, options ...grpc.CallOption) (*lnrpc.GetInfoResponse, error) { panic("not implemented") // TODO: Implement @@ -136,3 +140,7 @@ func (mock *lndSubscriptionStartMockClient) GetInfo(ctx context.Context, req *ln func (mock *lndSubscriptionStartMockClient) DecodeBolt11(ctx context.Context, bolt11 string, options ...grpc.CallOption) (*lnrpc.PayReq, error) { panic("not implemented") // TODO: Implement } + +func (mlnd *lndSubscriptionStartMockClient) TrackPayment(ctx context.Context, hash []byte, options ...grpc.CallOption) (*lnrpc.Payment, error) { + return nil, nil +} diff --git a/lib/service/checkpayments.go b/lib/service/checkpayments.go new file mode 100644 index 00000000..76a7538d --- /dev/null +++ b/lib/service/checkpayments.go @@ -0,0 +1,98 @@ +package service + +import ( + "context" + "encoding/hex" + "fmt" + + "github.com/getAlby/lndhub.go/db/models" + "github.com/getsentry/sentry-go" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" +) + +func (svc *LndhubService) CheckAllPendingOutgoingPayments(ctx context.Context) (err error) { + //check database for all pending payments + pendingPayments := []models.Invoice{} + //since this part is synchronously executed before the main server starts, we should not get into race conditions + //only fetch invoices from the last 2 weeks which should be a safe timeframe for hodl invoices to avoid refetching old invoices again and again + err = svc.DB.NewSelect().Model(&pendingPayments).Where("state = 'initialized'").Where("type = 'outgoing'").Where("r_hash != ''").Where("created_at >= (now() - interval '2 weeks') ").Scan(ctx) + if err != nil { + return err + } + svc.Logger.Infof("Found %d pending payments", len(pendingPayments)) + //call trackoutgoingpaymentstatus for each one + for _, inv := range pendingPayments { + //spawn goroutines + //https://go.dev/doc/faq#closures_and_goroutines + inv := inv + svc.Logger.Infof("Spawning tracker for payment with hash %s", inv.RHash) + go svc.TrackOutgoingPaymentstatus(ctx, &inv) + } + return nil +} + +// Should be called in a goroutine as the tracking can potentially take a long time +func (svc *LndhubService) TrackOutgoingPaymentstatus(ctx context.Context, invoice *models.Invoice) { + //ask lnd using TrackPaymentV2 by hash of payment + rawHash, err := hex.DecodeString(invoice.RHash) + if err != nil { + svc.Logger.Errorf("Error tracking payment %s: %s", invoice.RHash, err.Error()) + return + } + paymentTracker, err := svc.LndClient.SubscribePayment(ctx, &routerrpc.TrackPaymentRequest{ + PaymentHash: rawHash, + NoInflightUpdates: true, + }) + if err != nil { + svc.Logger.Errorf("Error tracking payment %s: %s", invoice.RHash, err.Error()) + return + } + //fetch the tx entry for the invoice + entry := models.TransactionEntry{} + //call HandleFailedPayment or HandleSuccesfulPayment + for { + payment, err := paymentTracker.Recv() + if err != nil { + svc.Logger.Errorf("Error tracking payment with hash %s: %s", invoice.RHash, err.Error()) + return + } + err = svc.DB.NewSelect().Model(&entry).Where("invoice_id = ?", invoice.ID).Limit(1).Scan(ctx) + if err != nil { + svc.Logger.Errorf("Error tracking payment %s: %s", invoice.RHash, err.Error()) + return + + } + if entry.UserID != invoice.UserID { + svc.Logger.Errorf("User ID's don't match : entry %v, invoice %v", entry, invoice) + return + } + if payment.Status == lnrpc.Payment_FAILED { + svc.Logger.Infof("Failed payment detected: hash %s, reason %s", payment.PaymentHash, payment.FailureReason) + err = svc.HandleFailedPayment(ctx, invoice, entry, fmt.Errorf(payment.FailureReason.String())) + if err != nil { + sentry.CaptureException(err) + svc.Logger.Errorf("Error handling failed payment %s: %s", invoice.RHash, err.Error()) + return + } + svc.Logger.Infof("Updated failed payment: hash %s, reason %s", payment.PaymentHash, payment.FailureReason) + return + } + if payment.Status == lnrpc.Payment_SUCCEEDED { + invoice.Fee = payment.FeeSat + invoice.Preimage = payment.PaymentPreimage + svc.Logger.Infof("Completed payment detected: hash %s", payment.PaymentHash) + err = svc.HandleSuccessfulPayment(ctx, invoice, entry) + if err != nil { + sentry.CaptureException(err) + svc.Logger.Errorf("Error handling successful payment %s: %s", invoice.RHash, err.Error()) + return + } + svc.Logger.Infof("Updated completed payment: hash %s", payment.PaymentHash) + return + } + //Since we shouldn't get in-flight updates we shouldn't get here + sentry.CaptureException(fmt.Errorf("Got an unexpected payment update %v", payment)) + svc.Logger.Warnf("Got an unexpected in-flight update %v", payment) + } +} diff --git a/lib/service/invoices.go b/lib/service/invoices.go index f370cae1..bcc701a3 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -253,7 +253,7 @@ func (svc *LndhubService) HandleFailedPayment(ctx context.Context, invoice *mode _, err := svc.DB.NewInsert().Model(&entry).Exec(ctx) if err != nil { sentry.CaptureException(err) - svc.Logger.Errorf("Could not insert transaction entry user_id:%v invoice_id:%v", invoice.UserID, invoice.ID) + svc.Logger.Errorf("Could not insert transaction entry user_id:%v invoice_id:%v error %s", invoice.UserID, invoice.ID, err.Error()) return err } @@ -265,7 +265,7 @@ func (svc *LndhubService) HandleFailedPayment(ctx context.Context, invoice *mode _, err = svc.DB.NewUpdate().Model(invoice).WherePK().Exec(ctx) if err != nil { sentry.CaptureException(err) - svc.Logger.Errorf("Could not update failed payment invoice user_id:%v invoice_id:%v", invoice.UserID, invoice.ID) + svc.Logger.Errorf("Could not update failed payment invoice user_id:%v invoice_id:%v error %s", invoice.UserID, invoice.ID, err.Error()) } return err } @@ -277,7 +277,7 @@ func (svc *LndhubService) HandleSuccessfulPayment(ctx context.Context, invoice * _, err := svc.DB.NewUpdate().Model(invoice).WherePK().Exec(ctx) if err != nil { sentry.CaptureException(err) - svc.Logger.Errorf("Could not update sucessful payment invoice user_id:%v invoice_id:%v", invoice.UserID, invoice.ID) + svc.Logger.Errorf("Could not update sucessful payment invoice user_id:%v invoice_id:%v, error %s", invoice.UserID, invoice.ID, err.Error()) } // Get the user's fee account for the transaction entry, current account is already there in parent entry @@ -299,14 +299,14 @@ func (svc *LndhubService) HandleSuccessfulPayment(ctx context.Context, invoice * _, err = svc.DB.NewInsert().Model(&entry).Exec(ctx) if err != nil { sentry.CaptureException(err) - svc.Logger.Errorf("Could not insert fee transaction entry user_id:%v invoice_id:%v", invoice.UserID, invoice.ID) + svc.Logger.Errorf("Could not insert fee transaction entry user_id:%v invoice_id:%v error %s", invoice.UserID, invoice.ID, err.Error()) return err } userBalance, err := svc.CurrentUserBalance(ctx, entry.UserID) if err != nil { sentry.CaptureException(err) - svc.Logger.Errorf("Could not fetch user balance user_id:%v invoice_id:%v", invoice.UserID, invoice.ID) + svc.Logger.Errorf("Could not fetch user balance user_id:%v invoice_id:%v error %s", invoice.UserID, invoice.ID, err.Error()) return err } diff --git a/lnd/interface.go b/lnd/interface.go index 0c3733b4..e7de0502 100644 --- a/lnd/interface.go +++ b/lnd/interface.go @@ -4,6 +4,7 @@ import ( "context" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "google.golang.org/grpc" ) @@ -12,6 +13,7 @@ type LightningClientWrapper interface { SendPaymentSync(ctx context.Context, req *lnrpc.SendRequest, options ...grpc.CallOption) (*lnrpc.SendResponse, error) AddInvoice(ctx context.Context, req *lnrpc.Invoice, options ...grpc.CallOption) (*lnrpc.AddInvoiceResponse, error) SubscribeInvoices(ctx context.Context, req *lnrpc.InvoiceSubscription, options ...grpc.CallOption) (SubscribeInvoicesWrapper, error) + SubscribePayment(ctx context.Context, req *routerrpc.TrackPaymentRequest, options ...grpc.CallOption) (SubscribePaymentWrapper, error) GetInfo(ctx context.Context, req *lnrpc.GetInfoRequest, options ...grpc.CallOption) (*lnrpc.GetInfoResponse, error) DecodeBolt11(ctx context.Context, bolt11 string, options ...grpc.CallOption) (*lnrpc.PayReq, error) } @@ -19,3 +21,6 @@ type LightningClientWrapper interface { type SubscribeInvoicesWrapper interface { Recv() (*lnrpc.Invoice, error) } +type SubscribePaymentWrapper interface { + Recv() (*lnrpc.Payment, error) +} diff --git a/lnd/lnd.go b/lnd/lnd.go index bdfe887d..a95b4442 100644 --- a/lnd/lnd.go +++ b/lnd/lnd.go @@ -9,6 +9,7 @@ import ( "io/ioutil" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/macaroons" "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -30,7 +31,8 @@ type LNDoptions struct { } type LNDWrapper struct { - client lnrpc.LightningClient + client lnrpc.LightningClient + routerClient routerrpc.RouterClient } func NewLNDclient(lndOptions LNDoptions) (result *LNDWrapper, err error) { @@ -92,7 +94,8 @@ func NewLNDclient(lndOptions LNDoptions) (result *LNDWrapper, err error) { } return &LNDWrapper{ - client: lnrpc.NewLightningClient(conn), + client: lnrpc.NewLightningClient(conn), + routerClient: routerrpc.NewRouterClient(conn), }, nil } @@ -121,3 +124,7 @@ func (wrapper *LNDWrapper) DecodeBolt11(ctx context.Context, bolt11 string, opti PayReq: bolt11, }) } + +func (wrapper *LNDWrapper) SubscribePayment(ctx context.Context, req *routerrpc.TrackPaymentRequest, options ...grpc.CallOption) (SubscribePaymentWrapper, error) { + return wrapper.routerClient.TrackPaymentV2(ctx, req, options...) +} diff --git a/main.go b/main.go index 1809ffa6..36e00242 100644 --- a/main.go +++ b/main.go @@ -165,6 +165,13 @@ func main() { // Subscribe to LND invoice updates in the background go svc.InvoiceUpdateSubscription(context.Background()) + // Check the status of all pending outgoing payments + // A goroutine will be spawned for each one + err = svc.CheckAllPendingOutgoingPayments(context.Background()) + if err != nil { + svc.Logger.Error(err) + } + //Start webhook subscription if svc.Config.WebhookUrl != "" { webhookCtx, cancelWebhook := context.WithCancel(context.Background())