From 61966e3d1c9a4c9c090ff721893fa562d15ec2fb Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Wed, 16 Feb 2022 09:12:37 +0100 Subject: [PATCH 01/24] address PR comments --- lib/service/invoices.go | 2 +- lnd/lnd.go | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/service/invoices.go b/lib/service/invoices.go index c69a55d..2ab6f5a 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -227,7 +227,7 @@ func (svc *LndhubService) AddOutgoingInvoice(ctx context.Context, userID int64, DestinationPubkeyHex: decodedInvoice.Destination, DescriptionHash: decodedInvoice.DescriptionHash, Memo: decodedInvoice.Description, - ExpiresAt: bun.NullTime{Time: time.Unix(decodedInvoice.Timestamp, 0).Add(time.Duration(decodedInvoice.Expiry))}, + ExpiresAt: bun.NullTime{Time: time.Unix(decodedInvoice.Timestamp, 0).Add(time.Duration(decodedInvoice.Expiry) * time.Second)}, } // Save invoice diff --git a/lnd/lnd.go b/lnd/lnd.go index ac61a41..dbfe8c8 100644 --- a/lnd/lnd.go +++ b/lnd/lnd.go @@ -15,10 +15,6 @@ import ( "gopkg.in/macaroon.v2" ) -const ( - MSAT_PER_SAT = 1000 -) - // LNDoptions are the options for the connection to the lnd node. type LNDoptions struct { Address string From 26694c821fe8ea6e777521c84e352622c909a4ed Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Wed, 16 Feb 2022 11:23:30 +0100 Subject: [PATCH 02/24] remove transactions, add empty func that will handle failed payments --- lib/service/invoices.go | 41 ++++++++++++++--------------------------- 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/lib/service/invoices.go b/lib/service/invoices.go index 2ab6f5a..20bc6fb 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -2,7 +2,6 @@ package service import ( "context" - "database/sql" "encoding/hex" "errors" "math/rand" @@ -42,7 +41,7 @@ func (svc *LndhubService) FindInvoiceByPaymentHash(ctx context.Context, userId i return &invoice, nil } -func (svc *LndhubService) SendInternalPayment(ctx context.Context, tx *bun.Tx, invoice *models.Invoice) (SendPaymentResponse, error) { +func (svc *LndhubService) SendInternalPayment(ctx context.Context, invoice *models.Invoice) (SendPaymentResponse, error) { sendPaymentResponse := SendPaymentResponse{} //SendInternalPayment() // find invoice @@ -70,7 +69,7 @@ func (svc *LndhubService) SendInternalPayment(ctx context.Context, tx *bun.Tx, i DebitAccountID: recipientDebitAccount.ID, Amount: invoice.Amount, } - _, err = tx.NewInsert().Model(&recipientEntry).Exec(ctx) + _, err = svc.DB.NewInsert().Model(&recipientEntry).Exec(ctx) if err != nil { return sendPaymentResponse, err } @@ -89,7 +88,7 @@ func (svc *LndhubService) SendInternalPayment(ctx context.Context, tx *bun.Tx, i incomingInvoice.Internal = true // mark incoming invoice as internal, just for documentation/debugging incomingInvoice.State = common.InvoiceStateSettled incomingInvoice.SettledAt = schema.NullTime{Time: time.Now()} - _, err = tx.NewUpdate().Model(&incomingInvoice).WherePK().Exec(ctx) + _, err = svc.DB.NewUpdate().Model(&incomingInvoice).WherePK().Exec(ctx) if err != nil { // could not save the invoice of the recipient return sendPaymentResponse, err @@ -98,7 +97,7 @@ func (svc *LndhubService) SendInternalPayment(ctx context.Context, tx *bun.Tx, i return sendPaymentResponse, nil } -func (svc *LndhubService) SendPaymentSync(ctx context.Context, tx *bun.Tx, invoice *models.Invoice) (SendPaymentResponse, error) { +func (svc *LndhubService) SendPaymentSync(ctx context.Context, invoice *models.Invoice) (SendPaymentResponse, error) { sendPaymentResponse := SendPaymentResponse{} // TODO: set dynamic fee limit feeLimit := lnrpc.FeeLimit{ @@ -159,18 +158,10 @@ func (svc *LndhubService) PayInvoice(ctx context.Context, invoice *models.Invoic Amount: invoice.Amount, } - // Start a DB transaction - // We rollback anything on error (only the invoice that was passed in to the PayInvoice calls stays in the DB) - tx, err := svc.DB.BeginTx(ctx, &sql.TxOptions{}) - if err != nil { - return nil, err - } - // 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(ctx) + _, err = svc.DB.NewInsert().Model(&entry).Exec(ctx) if err != nil { - tx.Rollback() return nil, err } @@ -179,15 +170,15 @@ func (svc *LndhubService) PayInvoice(ctx context.Context, invoice *models.Invoic var paymentResponse SendPaymentResponse // Check the destination pubkey if it is an internal invoice and going to our node if svc.IdentityPubkey == invoice.DestinationPubkeyHex { - paymentResponse, err = svc.SendInternalPayment(ctx, &tx, invoice) + paymentResponse, err = svc.SendInternalPayment(ctx, invoice) if err != nil { - tx.Rollback() + svc.HandleFailedPayment(invoice, err) return nil, err } } else { - paymentResponse, err = svc.SendPaymentSync(ctx, &tx, invoice) + paymentResponse, err = svc.SendPaymentSync(ctx, invoice) if err != nil { - tx.Rollback() + svc.HandleFailedPayment(invoice, err) return nil, err } } @@ -199,15 +190,7 @@ func (svc *LndhubService) PayInvoice(ctx context.Context, invoice *models.Invoic invoice.State = common.InvoiceStateSettled invoice.SettledAt = schema.NullTime{Time: time.Now()} - _, err = tx.NewUpdate().Model(invoice).WherePK().Exec(ctx) - if err != nil { - tx.Rollback() - return nil, err - } - - // Commit the DB transaction. Done, everything worked - err = tx.Commit() - + _, err = svc.DB.NewUpdate().Model(invoice).WherePK().Exec(ctx) if err != nil { return nil, err } @@ -215,6 +198,10 @@ func (svc *LndhubService) PayInvoice(ctx context.Context, invoice *models.Invoic return &paymentResponse, err } +func (svc *LndhubService) HandleFailedPayment(invoice *models.Invoice, err error) { + //what if the error was due to canceled ctx? +} + func (svc *LndhubService) AddOutgoingInvoice(ctx context.Context, userID int64, paymentRequest string, decodedInvoice *lnrpc.PayReq) (*models.Invoice, error) { // Initialize new DB invoice invoice := models.Invoice{ From 7ff25113e5d7eb3909ce07a45e3a95670e2a5e36 Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Wed, 16 Feb 2022 12:29:11 +0100 Subject: [PATCH 03/24] outline behaviour in comments --- lib/service/invoices.go | 29 +++++++++++++++++++++++++---- lib/service/invoicesubscription.go | 17 +++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/lib/service/invoices.go b/lib/service/invoices.go index 20bc6fb..25c58ef 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -172,13 +172,13 @@ func (svc *LndhubService) PayInvoice(ctx context.Context, invoice *models.Invoic if svc.IdentityPubkey == invoice.DestinationPubkeyHex { paymentResponse, err = svc.SendInternalPayment(ctx, invoice) if err != nil { - svc.HandleFailedPayment(invoice, err) + svc.HandleFailedPayment(ctx, invoice, err) return nil, err } } else { paymentResponse, err = svc.SendPaymentSync(ctx, invoice) if err != nil { - svc.HandleFailedPayment(invoice, err) + svc.HandleFailedPayment(ctx, invoice, err) return nil, err } } @@ -198,8 +198,29 @@ func (svc *LndhubService) PayInvoice(ctx context.Context, invoice *models.Invoic return &paymentResponse, err } -func (svc *LndhubService) HandleFailedPayment(invoice *models.Invoice, err error) { - //what if the error was due to canceled ctx? +//this method should be called on 2 occasions: when the sendpayment calls returns with an error +//and in an async goroutine that subscribes to outgoing payments. +func (svc *LndhubService) HandleFailedPayment(ctx context.Context, invoice *models.Invoice, err error) error { + //if the error was due to canceled ctx, we don't know anything + //about the payment, so we don't do anything here, it will be + //handled asynchronously in the goroutine that subscribes to + //outgoing payments + if err.Error() == context.Canceled.Error() { + return nil + } + //if we get here, we can be sure that the payment actually failed + //so we must 1) add a new transactionentry that transfers + //funds back to the user's "current" balance 2) update the outgoing + //invoice with the error message and mark it as failed + return nil +} + +//this method should be called on 2 occasions: when the sendpayment calls returns without an error +//and in an async goroutine that subscribes to outgoing payments. +func (svc *LndhubService) HandleSuccesfulPayment(ctx context.Context, invoice *models.Invoice, err error) error { + //here we should just update the outgoing invoice as completed + //so consolidate the last couple of lines from SendPaymentInternal/SendPaymentSync here + return nil } func (svc *LndhubService) AddOutgoingInvoice(ctx context.Context, userID int64, paymentRequest string, decodedInvoice *lnrpc.PayReq) (*models.Invoice, error) { diff --git a/lib/service/invoicesubscription.go b/lib/service/invoicesubscription.go index e809be8..a1ec79a 100644 --- a/lib/service/invoicesubscription.go +++ b/lib/service/invoicesubscription.go @@ -150,3 +150,20 @@ func (svc *LndhubService) InvoiceUpdateSubscription(ctx context.Context) error { } } } + +//TODO: implement payment update subscription +//make sure that we do not conflict if the sync +//handling is being done at the same time. +func (svc *LndhubService) PaymentUpdateSubscription(ctx context.Context) error { + // paymentSubscriptionStream, err := svc.ConnectPaymentSubscription(ctx) + // if err != nil { + // sentry.CaptureException(err) + // return err + // } + // for { + // payment, err := paymentSubscriptionStream.Recv() + // if payment success: svc.HandlePaymentSuccess + // if payment fail: svc.HandlePaymentFailure + // } + return nil +} From 5c2d88d2f7611132c30e012416c837c6cf2058c8 Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Wed, 16 Feb 2022 12:33:57 +0100 Subject: [PATCH 04/24] add desired integration test behaviour --- integration_tests/outgoing_payment_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/integration_tests/outgoing_payment_test.go b/integration_tests/outgoing_payment_test.go index 6f24cb6..776aacc 100644 --- a/integration_tests/outgoing_payment_test.go +++ b/integration_tests/outgoing_payment_test.go @@ -34,3 +34,13 @@ func (suite *PaymentTestSuite) TestOutGoingPayment() { payResponse := suite.createPayInvoiceReq(invoice.PaymentRequest, suite.aliceToken) assert.NotEmpty(suite.T(), payResponse.PaymentPreimage) } + +func (suite *PaymentTestSuite) TestOutGoingPaymentFailure() { + //TODO: use a dummy LND interface to test different scenarios + //might be better if this has it's own suite + //because we need a different LND client + // - payment fails directly, no callback event + // - payment fails after some time, failure callback event + // - payment call never returns, success callback event + // - payment call never returns, failure callback event +} From 74da8f808809076a33faa517f515aa535eb09d0d Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Thu, 17 Feb 2022 11:31:39 +0100 Subject: [PATCH 05/24] new pr scope: don't bother with callbacks --- integration_tests/outgoing_payment_test.go | 9 ++++----- lib/service/invoices.go | 19 +++++-------------- lib/service/invoicesubscription.go | 17 ----------------- 3 files changed, 9 insertions(+), 36 deletions(-) diff --git a/integration_tests/outgoing_payment_test.go b/integration_tests/outgoing_payment_test.go index 776aacc..2ea3335 100644 --- a/integration_tests/outgoing_payment_test.go +++ b/integration_tests/outgoing_payment_test.go @@ -36,11 +36,10 @@ func (suite *PaymentTestSuite) TestOutGoingPayment() { } func (suite *PaymentTestSuite) TestOutGoingPaymentFailure() { - //TODO: use a dummy LND interface to test different scenarios + //TODO: use a new implementation of LNDClientWrapper interface to test different scenarios //might be better if this has it's own suite //because we need a different LND client - // - payment fails directly, no callback event - // - payment fails after some time, failure callback event - // - payment call never returns, success callback event - // - payment call never returns, failure callback event + // - payment fails directly + // - payment fails after some time, check that balance is locked in the meantime and is restored afterwards + // - payment call succeeds after some time, check that balance is locked in the meantime and is _not_ restored afterwards } diff --git a/lib/service/invoices.go b/lib/service/invoices.go index 25c58ef..503f3af 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -169,16 +169,18 @@ func (svc *LndhubService) PayInvoice(ctx context.Context, invoice *models.Invoic var paymentResponse SendPaymentResponse // Check the destination pubkey if it is an internal invoice and going to our node + // Here we start using context.Background because we want to complete these calls + // regardless of if the request's context is canceled or not. if svc.IdentityPubkey == invoice.DestinationPubkeyHex { - paymentResponse, err = svc.SendInternalPayment(ctx, invoice) + paymentResponse, err = svc.SendInternalPayment(context.Background(), invoice) if err != nil { svc.HandleFailedPayment(ctx, invoice, err) return nil, err } } else { - paymentResponse, err = svc.SendPaymentSync(ctx, invoice) + paymentResponse, err = svc.SendPaymentSync(context.Background(), invoice) if err != nil { - svc.HandleFailedPayment(ctx, invoice, err) + svc.HandleFailedPayment(context.Background(), invoice, err) return nil, err } } @@ -198,16 +200,7 @@ func (svc *LndhubService) PayInvoice(ctx context.Context, invoice *models.Invoic return &paymentResponse, err } -//this method should be called on 2 occasions: when the sendpayment calls returns with an error -//and in an async goroutine that subscribes to outgoing payments. func (svc *LndhubService) HandleFailedPayment(ctx context.Context, invoice *models.Invoice, err error) error { - //if the error was due to canceled ctx, we don't know anything - //about the payment, so we don't do anything here, it will be - //handled asynchronously in the goroutine that subscribes to - //outgoing payments - if err.Error() == context.Canceled.Error() { - return nil - } //if we get here, we can be sure that the payment actually failed //so we must 1) add a new transactionentry that transfers //funds back to the user's "current" balance 2) update the outgoing @@ -215,8 +208,6 @@ func (svc *LndhubService) HandleFailedPayment(ctx context.Context, invoice *mode return nil } -//this method should be called on 2 occasions: when the sendpayment calls returns without an error -//and in an async goroutine that subscribes to outgoing payments. func (svc *LndhubService) HandleSuccesfulPayment(ctx context.Context, invoice *models.Invoice, err error) error { //here we should just update the outgoing invoice as completed //so consolidate the last couple of lines from SendPaymentInternal/SendPaymentSync here diff --git a/lib/service/invoicesubscription.go b/lib/service/invoicesubscription.go index a1ec79a..e809be8 100644 --- a/lib/service/invoicesubscription.go +++ b/lib/service/invoicesubscription.go @@ -150,20 +150,3 @@ func (svc *LndhubService) InvoiceUpdateSubscription(ctx context.Context) error { } } } - -//TODO: implement payment update subscription -//make sure that we do not conflict if the sync -//handling is being done at the same time. -func (svc *LndhubService) PaymentUpdateSubscription(ctx context.Context) error { - // paymentSubscriptionStream, err := svc.ConnectPaymentSubscription(ctx) - // if err != nil { - // sentry.CaptureException(err) - // return err - // } - // for { - // payment, err := paymentSubscriptionStream.Recv() - // if payment success: svc.HandlePaymentSuccess - // if payment fail: svc.HandlePaymentFailure - // } - return nil -} From 1d9e24f21c545dde2c85d9c369be63f3491b3b1e Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Thu, 17 Feb 2022 14:27:49 +0100 Subject: [PATCH 06/24] Implement handle successful payment method --- lib/service/invoices.go | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/lib/service/invoices.go b/lib/service/invoices.go index 503f3af..f2e317f 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -189,14 +189,7 @@ func (svc *LndhubService) PayInvoice(ctx context.Context, invoice *models.Invoic // The payment was successful. invoice.Preimage = paymentResponse.PaymentPreimageStr - invoice.State = common.InvoiceStateSettled - invoice.SettledAt = schema.NullTime{Time: time.Now()} - - _, err = svc.DB.NewUpdate().Model(invoice).WherePK().Exec(ctx) - if err != nil { - return nil, err - } - + err = svc.HandleSuccessfulPayment(ctx, invoice) return &paymentResponse, err } @@ -208,10 +201,13 @@ func (svc *LndhubService) HandleFailedPayment(ctx context.Context, invoice *mode return nil } -func (svc *LndhubService) HandleSuccesfulPayment(ctx context.Context, invoice *models.Invoice, err error) error { - //here we should just update the outgoing invoice as completed - //so consolidate the last couple of lines from SendPaymentInternal/SendPaymentSync here - return nil +func (svc *LndhubService) HandleSuccessfulPayment(ctx context.Context, invoice *models.Invoice) error { + invoice.State = common.InvoiceStateSettled + invoice.SettledAt = schema.NullTime{Time: time.Now()} + + _, err := svc.DB.NewUpdate().Model(invoice).WherePK().Exec(ctx) + // TODO: error logging + return err } func (svc *LndhubService) AddOutgoingInvoice(ctx context.Context, userID int64, paymentRequest string, decodedInvoice *lnrpc.PayReq) (*models.Invoice, error) { From fea16623ab57ccd1f92053d22e4dd049a7e72884 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Thu, 17 Feb 2022 15:10:42 +0100 Subject: [PATCH 07/24] Implement handle failed payment method --- common/globals.go | 1 + lib/service/invoices.go | 31 +++++++++++++++++++++++-------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/common/globals.go b/common/globals.go index 1b4f6d8..b53b18b 100644 --- a/common/globals.go +++ b/common/globals.go @@ -9,6 +9,7 @@ const ( InvoiceStateSettled = "settled" InvoiceStateInitialized = "initialized" InvoiceStateOpen = "open" + InvoiceStateError = "error" AccountTypeIncoming = "incoming" AccountTypeCurrent = "current" diff --git a/lib/service/invoices.go b/lib/service/invoices.go index f2e317f..0b33a44 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -174,13 +174,13 @@ func (svc *LndhubService) PayInvoice(ctx context.Context, invoice *models.Invoic if svc.IdentityPubkey == invoice.DestinationPubkeyHex { paymentResponse, err = svc.SendInternalPayment(context.Background(), invoice) if err != nil { - svc.HandleFailedPayment(ctx, invoice, err) + svc.HandleFailedPayment(ctx, invoice, entry, err) return nil, err } } else { paymentResponse, err = svc.SendPaymentSync(context.Background(), invoice) if err != nil { - svc.HandleFailedPayment(context.Background(), invoice, err) + svc.HandleFailedPayment(context.Background(), invoice, entry, err) return nil, err } } @@ -193,12 +193,27 @@ func (svc *LndhubService) PayInvoice(ctx context.Context, invoice *models.Invoic return &paymentResponse, err } -func (svc *LndhubService) HandleFailedPayment(ctx context.Context, invoice *models.Invoice, err error) error { - //if we get here, we can be sure that the payment actually failed - //so we must 1) add a new transactionentry that transfers - //funds back to the user's "current" balance 2) update the outgoing - //invoice with the error message and mark it as failed - return nil +func (svc *LndhubService) HandleFailedPayment(ctx context.Context, invoice *models.Invoice, entryToRevert models.TransactionEntry, err error) error { + // add transaction entry with reverted credit/debit account id + entry := models.TransactionEntry{ + UserID: invoice.UserID, + InvoiceID: invoice.ID, + CreditAccountID: entryToRevert.DebitAccountID, + DebitAccountID: entryToRevert.CreditAccountID, + Amount: invoice.Amount, + } + _, err = svc.DB.NewInsert().Model(&entry).Exec(ctx) + if err != nil { + // TODO: error logging + return err + } + + // TODO: maybe save errors on the invoice? + invoice.State = common.InvoiceStateError + + _, err = svc.DB.NewUpdate().Model(invoice).WherePK().Exec(ctx) + // TODO: error logging + return err } func (svc *LndhubService) HandleSuccessfulPayment(ctx context.Context, invoice *models.Invoice) error { From 2e5f411cf1f4c3ea13d96daa509b17afcd2c97d3 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Thu, 17 Feb 2022 15:17:05 +0100 Subject: [PATCH 08/24] Use context background on all needed places --- lib/service/invoices.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/service/invoices.go b/lib/service/invoices.go index 0b33a44..85eed14 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -174,7 +174,7 @@ func (svc *LndhubService) PayInvoice(ctx context.Context, invoice *models.Invoic if svc.IdentityPubkey == invoice.DestinationPubkeyHex { paymentResponse, err = svc.SendInternalPayment(context.Background(), invoice) if err != nil { - svc.HandleFailedPayment(ctx, invoice, entry, err) + svc.HandleFailedPayment(context.Background(), invoice, entry, err) return nil, err } } else { @@ -189,7 +189,7 @@ func (svc *LndhubService) PayInvoice(ctx context.Context, invoice *models.Invoic // The payment was successful. invoice.Preimage = paymentResponse.PaymentPreimageStr - err = svc.HandleSuccessfulPayment(ctx, invoice) + err = svc.HandleSuccessfulPayment(context.Background(), invoice) return &paymentResponse, err } From 17e42f42eacb5d4ca40b9801fb6eabf5846d530e Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Thu, 17 Feb 2022 16:05:09 +0100 Subject: [PATCH 09/24] Add tear down test to create tests --- integration_tests/create_test.go | 9 +++++++++ integration_tests/util.go | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/integration_tests/create_test.go b/integration_tests/create_test.go index 275e537..0de44ac 100644 --- a/integration_tests/create_test.go +++ b/integration_tests/create_test.go @@ -36,6 +36,15 @@ func (suite *CreateUserTestSuite) TearDownSuite() { } +func (suite *CreateUserTestSuite) TearDownTest() { + err := clearTable(suite.Service, "users") + if err != nil { + fmt.Printf("Tear down test error %v\n", err.Error()) + return + } + fmt.Println("Tear down test success") +} + func (suite *CreateUserTestSuite) TestCreate() { e := echo.New() e.HTTPErrorHandler = responses.HTTPErrorHandler diff --git a/integration_tests/util.go b/integration_tests/util.go index 38f5cd8..8513471 100644 --- a/integration_tests/util.go +++ b/integration_tests/util.go @@ -74,6 +74,16 @@ func LndHubTestServiceInit() (svc *service.LndhubService, err error) { return svc, nil } +func clearTable(svc *service.LndhubService, tableName string) error { + dbConn, err := db.Open(svc.Config.DatabaseUri) + if err != nil { + return fmt.Errorf("failed to connect to database: %w", err) + } + + _, err = dbConn.Exec(fmt.Sprintf("DELETE FROM %s", tableName)) + return err +} + func createUsers(svc *service.LndhubService, usersToCreate int) (logins []controllers.CreateUserResponseBody, tokens []string, err error) { logins = []controllers.CreateUserResponseBody{} tokens = []string{} From 63a3ea09b34ab8965b3c3cee292a9ccd67052106 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Thu, 17 Feb 2022 18:32:30 +0100 Subject: [PATCH 10/24] Add simple failure test --- integration_tests/payment_failure_test.go | 108 ++++++++++++++++++++++ integration_tests/util.go | 9 ++ lib/service/invoices.go | 3 +- lib/service/user.go | 6 ++ 4 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 integration_tests/payment_failure_test.go diff --git a/integration_tests/payment_failure_test.go b/integration_tests/payment_failure_test.go new file mode 100644 index 0000000..7c88068 --- /dev/null +++ b/integration_tests/payment_failure_test.go @@ -0,0 +1,108 @@ +package integration_tests + +import ( + "context" + "fmt" + "log" + "testing" + + "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/getAlby/lndhub.go/lnd" + "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 PaymentTestErrorsSuite struct { + TestSuite + fundingClient *lnd.LNDWrapper + service *service.LndhubService + aliceLogin controllers.CreateUserResponseBody + aliceToken string + bobLogin controllers.CreateUserResponseBody + bobToken string + bobId string +} + +func (suite *PaymentTestErrorsSuite) SetupSuite() { + lndClient, err := lnd.NewLNDclient(lnd.LNDoptions{ + Address: lnd2RegtestAddress, + MacaroonHex: lnd2RegtestMacaroonHex, + }) + if err != nil { + log.Fatalf("Error setting up funding client: %v", err) + } + suite.fundingClient = lndClient + + svc, err := LndHubTestServiceInit() + if err != nil { + log.Fatalf("Error initializing test service: %v", err) + } + users, userTokens, err := createUsers(svc, 2) + if err != nil { + log.Fatalf("Error creating test users: %v", err) + } + // Subscribe to LND invoice updates in the background + go svc.InvoiceUpdateSubscription(context.Background()) + suite.service = svc + e := echo.New() + + e.HTTPErrorHandler = responses.HTTPErrorHandler + e.Validator = &lib.CustomValidator{Validator: validator.New()} + suite.echo = e + assert.Equal(suite.T(), 2, len(users)) + assert.Equal(suite.T(), 2, len(userTokens)) + suite.aliceLogin = users[0] + suite.aliceToken = userTokens[0] + suite.bobLogin = users[1] + suite.bobToken = userTokens[1] + 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 *PaymentTestErrorsSuite) TestExternalFailingInvoice() { + //create external zero amount invoice that will fail + externalInvoice := lnrpc.Invoice{ + Memo: "integration tests: external failing pay", + Value: 0, + } + invoice, err := suite.fundingClient.AddInvoice(context.Background(), &externalInvoice) + assert.NoError(suite.T(), err) + _ = suite.createPayInvoiceReqError(invoice.PaymentRequest, suite.bobToken) + + userId := getUserIdFromToken(suite.bobToken) + + 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) + + 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 2 transaction entries, with reversed credit and debit account ids + assert.Equal(suite.T(), 2, len(transactonEntries)) + assert.Equal(suite.T(), transactonEntries[0].CreditAccountID, transactonEntries[1].DebitAccountID) + assert.Equal(suite.T(), transactonEntries[0].DebitAccountID, transactonEntries[1].CreditAccountID) +} + +func (suite *PaymentTestErrorsSuite) TearDownSuite() { + +} + +func TestPaymentTestErrorsSuite(t *testing.T) { + suite.Run(t, new(PaymentTestErrorsSuite)) +} diff --git a/integration_tests/util.go b/integration_tests/util.go index 8513471..82503d9 100644 --- a/integration_tests/util.go +++ b/integration_tests/util.go @@ -15,6 +15,7 @@ import ( "github.com/getAlby/lndhub.go/lib/responses" "github.com/getAlby/lndhub.go/lib/service" "github.com/getAlby/lndhub.go/lnd" + "github.com/golang-jwt/jwt" "github.com/labstack/echo/v4" "github.com/lightningnetwork/lnd/lnrpc" "github.com/stretchr/testify/assert" @@ -84,6 +85,14 @@ func clearTable(svc *service.LndhubService, tableName string) error { return err } +// unsafe parse jwt method to pull out userId claim +// should be used only in integration_tests package +func getUserIdFromToken(token string) int64 { + parsedToken, _, _ := new(jwt.Parser).ParseUnverified(token, jwt.MapClaims{}) + claims, _ := parsedToken.Claims.(jwt.MapClaims) + return int64(claims["id"].(float64)) +} + func createUsers(svc *service.LndhubService, usersToCreate int) (logins []controllers.CreateUserResponseBody, tokens []string, err error) { logins = []controllers.CreateUserResponseBody{} tokens = []string{} diff --git a/lib/service/invoices.go b/lib/service/invoices.go index 85eed14..b1a9182 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -43,7 +43,6 @@ func (svc *LndhubService) FindInvoiceByPaymentHash(ctx context.Context, userId i func (svc *LndhubService) SendInternalPayment(ctx context.Context, invoice *models.Invoice) (SendPaymentResponse, error) { sendPaymentResponse := SendPaymentResponse{} - //SendInternalPayment() // find invoice var incomingInvoice models.Invoice err := svc.DB.NewSelect().Model(&incomingInvoice).Where("type = ? AND payment_request = ? AND state = ? ", common.InvoiceTypeIncoming, invoice.PaymentRequest, common.InvoiceStateOpen).Limit(1).Scan(ctx) @@ -122,7 +121,7 @@ func (svc *LndhubService) SendPaymentSync(ctx context.Context, invoice *models.I return sendPaymentResponse, err } - // If there was a payment error we rollback and return an error + // If there was a payment error we return an error if sendPaymentResult.GetPaymentError() != "" || sendPaymentResult.GetPaymentPreimage() == nil { return sendPaymentResponse, errors.New(sendPaymentResult.GetPaymentError()) } diff --git a/lib/service/user.go b/lib/service/user.go index ad9c471..766a3bb 100644 --- a/lib/service/user.go +++ b/lib/service/user.go @@ -82,6 +82,12 @@ func (svc *LndhubService) AccountFor(ctx context.Context, accountType string, us return account, err } +func (svc *LndhubService) TransactionEntriesFor(ctx context.Context, userId int64) ([]models.TransactionEntry, error) { + transactionEntries := []models.TransactionEntry{} + err := svc.DB.NewSelect().Model(&transactionEntries).Where("user_id = ?", userId).Scan(ctx) + return transactionEntries, err +} + func (svc *LndhubService) InvoicesFor(ctx context.Context, userId int64, invoiceType string) ([]models.Invoice, error) { var invoices []models.Invoice From 720a40061e531f3326207987fd8b7fddb7702575 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Thu, 17 Feb 2022 19:47:08 +0100 Subject: [PATCH 11/24] Add and use lnd mock with failing send payment sync --- integration_tests/auth_test.go | 2 +- integration_tests/create_test.go | 2 +- integration_tests/gettxs_test.go | 2 +- integration_tests/incoming_payment_test.go | 2 +- integration_tests/internal_payment_test.go | 2 +- integration_tests/lnd-mock.go | 29 ++++++++++ integration_tests/outgoing_payment_test.go | 16 ++++++ integration_tests/payment_failure_test.go | 66 +++++++++++++++++----- integration_tests/util.go | 19 ++++--- 9 files changed, 114 insertions(+), 26 deletions(-) create mode 100644 integration_tests/lnd-mock.go diff --git a/integration_tests/auth_test.go b/integration_tests/auth_test.go index e453928..bd92753 100644 --- a/integration_tests/auth_test.go +++ b/integration_tests/auth_test.go @@ -27,7 +27,7 @@ type UserAuthTestSuite struct { } func (suite *UserAuthTestSuite) SetupSuite() { - svc, err := LndHubTestServiceInit() + svc, err := LndHubTestServiceInit(nil) if err != nil { log.Fatalf("Error initializing test service: %v", err) } diff --git a/integration_tests/create_test.go b/integration_tests/create_test.go index 0de44ac..fdaa3dd 100644 --- a/integration_tests/create_test.go +++ b/integration_tests/create_test.go @@ -25,7 +25,7 @@ type CreateUserTestSuite struct { } func (suite *CreateUserTestSuite) SetupSuite() { - svc, err := LndHubTestServiceInit() + svc, err := LndHubTestServiceInit(nil) if err != nil { log.Fatalf("Error initializing test service: %v", err) } diff --git a/integration_tests/gettxs_test.go b/integration_tests/gettxs_test.go index f490244..72b71cf 100644 --- a/integration_tests/gettxs_test.go +++ b/integration_tests/gettxs_test.go @@ -42,7 +42,7 @@ func (suite *GetTxTestSuite) SetupSuite() { } suite.fundingClient = lndClient - svc, err := LndHubTestServiceInit() + svc, err := LndHubTestServiceInit(nil) if err != nil { log.Fatalf("Error initializing test service: %v", err) } diff --git a/integration_tests/incoming_payment_test.go b/integration_tests/incoming_payment_test.go index 5762c92..209287d 100644 --- a/integration_tests/incoming_payment_test.go +++ b/integration_tests/incoming_payment_test.go @@ -47,7 +47,7 @@ func (suite *IncomingPaymentTestSuite) SetupSuite() { } suite.fundingClient = lndClient - svc, err := LndHubTestServiceInit() + svc, err := LndHubTestServiceInit(nil) if err != nil { log.Fatalf("Error initializing test service: %v", err) } diff --git a/integration_tests/internal_payment_test.go b/integration_tests/internal_payment_test.go index 4105504..7aec678 100644 --- a/integration_tests/internal_payment_test.go +++ b/integration_tests/internal_payment_test.go @@ -39,7 +39,7 @@ func (suite *PaymentTestSuite) SetupSuite() { } suite.fundingClient = lndClient - svc, err := LndHubTestServiceInit() + svc, err := LndHubTestServiceInit(nil) if err != nil { log.Fatalf("Error initializing test service: %v", err) } diff --git a/integration_tests/lnd-mock.go b/integration_tests/lnd-mock.go new file mode 100644 index 0000000..259b59b --- /dev/null +++ b/integration_tests/lnd-mock.go @@ -0,0 +1,29 @@ +package integration_tests + +import ( + "context" + "errors" + + "github.com/getAlby/lndhub.go/lnd" + "github.com/lightningnetwork/lnd/lnrpc" + "google.golang.org/grpc" +) + +type LNDMockWrapper struct { + *lnd.LNDWrapper +} + +func NewLNDMockWrapper(lndOptions lnd.LNDoptions) (result *LNDMockWrapper, err error) { + lnd, err := lnd.NewLNDclient(lndOptions) + if err != nil { + return nil, err + } + + return &LNDMockWrapper{ + LNDWrapper: lnd, + }, nil +} + +func (wrapper *LNDMockWrapper) SendPaymentSync(ctx context.Context, req *lnrpc.SendRequest, options ...grpc.CallOption) (*lnrpc.SendResponse, error) { + return nil, errors.New("mocked send payment error") +} diff --git a/integration_tests/outgoing_payment_test.go b/integration_tests/outgoing_payment_test.go index 2ea3335..423867c 100644 --- a/integration_tests/outgoing_payment_test.go +++ b/integration_tests/outgoing_payment_test.go @@ -2,6 +2,7 @@ package integration_tests import ( "context" + "fmt" "time" "github.com/lightningnetwork/lnd/lnrpc" @@ -33,6 +34,21 @@ func (suite *PaymentTestSuite) TestOutGoingPayment() { //pay external from alice payResponse := suite.createPayInvoiceReq(invoice.PaymentRequest, suite.aliceToken) assert.NotEmpty(suite.T(), payResponse.PaymentPreimage) + + // check that balance was reduced + userId := getUserIdFromToken(suite.aliceToken) + aliceBalance, 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(500), aliceBalance) + + // check that no additional transaction entry was created + transactonEntries, err := suite.service.TransactionEntriesFor(context.Background(), userId) + if err != nil { + fmt.Printf("Error when getting transaction entries %v\n", err.Error()) + } + assert.Equal(suite.T(), 2, len(transactonEntries)) } func (suite *PaymentTestSuite) TestOutGoingPaymentFailure() { diff --git a/integration_tests/payment_failure_test.go b/integration_tests/payment_failure_test.go index 7c88068..286b7d4 100644 --- a/integration_tests/payment_failure_test.go +++ b/integration_tests/payment_failure_test.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "testing" + "time" "github.com/getAlby/lndhub.go/common" "github.com/getAlby/lndhub.go/controllers" @@ -20,6 +21,11 @@ import ( "github.com/stretchr/testify/suite" ) +const ( + lnd1RegtestAddress = "rpc.lnd1.regtest.getalby.com:443" + lnd1RegtestMacaroonHex = "0201036c6e6402f801030a10e2133a1cac2c5b4d56e44e32dc64c8551201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620c4f9783e0873fa50a2091806f5ebb919c5dc432e33800b401463ada6485df0ed" +) + type PaymentTestErrorsSuite struct { TestSuite fundingClient *lnd.LNDWrapper @@ -28,20 +34,29 @@ type PaymentTestErrorsSuite struct { aliceToken string bobLogin controllers.CreateUserResponseBody bobToken string - bobId string } func (suite *PaymentTestErrorsSuite) SetupSuite() { - lndClient, err := lnd.NewLNDclient(lnd.LNDoptions{ + // use real client for funding only + fundingClient, err := lnd.NewLNDclient(lnd.LNDoptions{ Address: lnd2RegtestAddress, MacaroonHex: lnd2RegtestMacaroonHex, }) if err != nil { log.Fatalf("Error setting up funding client: %v", err) } - suite.fundingClient = lndClient - svc, err := LndHubTestServiceInit() + // inject fake lnd client with failing send payment sync into service + lndClient, err := NewLNDMockWrapper(lnd.LNDoptions{ + Address: lnd1RegtestAddress, + MacaroonHex: lnd1RegtestMacaroonHex, + }) + if err != nil { + log.Fatalf("Error setting up test client: %v", err) + } + suite.fundingClient = fundingClient + + svc, err := LndHubTestServiceInit(lndClient) if err != nil { log.Fatalf("Error initializing test service: %v", err) } @@ -70,16 +85,31 @@ func (suite *PaymentTestErrorsSuite) SetupSuite() { } func (suite *PaymentTestErrorsSuite) TestExternalFailingInvoice() { - //create external zero amount invoice that will fail + aliceFundingSats := 1000 + externalSatRequested := 500 + //fund alice account + invoiceResponse := suite.createAddInvoiceReq(aliceFundingSats, "integration test external payment alice", suite.aliceToken) + sendPaymentRequest := lnrpc.SendRequest{ + PaymentRequest: invoiceResponse.PayReq, + FeeLimit: nil, + } + _, err := suite.fundingClient.SendPaymentSync(context.Background(), &sendPaymentRequest) + assert.NoError(suite.T(), err) + + //wait a bit for the callback event to hit + time.Sleep(100 * time.Millisecond) + + //create external invoice externalInvoice := lnrpc.Invoice{ - Memo: "integration tests: external failing pay", - Value: 0, + Memo: "integration tests: external pay from alice", + Value: int64(externalSatRequested), } invoice, err := suite.fundingClient.AddInvoice(context.Background(), &externalInvoice) assert.NoError(suite.T(), err) - _ = suite.createPayInvoiceReqError(invoice.PaymentRequest, suite.bobToken) + //pay external from alice, mock will fail + _ = suite.createPayInvoiceReqError(invoice.PaymentRequest, suite.aliceToken) - userId := getUserIdFromToken(suite.bobToken) + userId := getUserIdFromToken(suite.aliceToken) invoices, err := suite.service.InvoicesFor(context.Background(), userId, common.InvoiceTypeOutgoing) if err != nil { @@ -93,12 +123,20 @@ func (suite *PaymentTestErrorsSuite) TestExternalFailingInvoice() { fmt.Printf("Error when getting transaction entries %v\n", err.Error()) } - // check if there are 2 transaction entries, with reversed credit and debit account ids - assert.Equal(suite.T(), 2, len(transactonEntries)) - assert.Equal(suite.T(), transactonEntries[0].CreditAccountID, transactonEntries[1].DebitAccountID) - assert.Equal(suite.T(), transactonEntries[0].DebitAccountID, transactonEntries[1].CreditAccountID) -} + aliceBalance, err := suite.service.CurrentUserBalance(context.Background(), userId) + if err != nil { + fmt.Printf("Error when getting balance %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)) + // assert that balance is the same + assert.Equal(suite.T(), int64(aliceFundingSats), aliceBalance) +} func (suite *PaymentTestErrorsSuite) TearDownSuite() { } diff --git a/integration_tests/util.go b/integration_tests/util.go index 82503d9..9adef9b 100644 --- a/integration_tests/util.go +++ b/integration_tests/util.go @@ -23,7 +23,7 @@ import ( "github.com/uptrace/bun/migrate" ) -func LndHubTestServiceInit() (svc *service.LndhubService, err error) { +func LndHubTestServiceInit(lndClientMock *LNDMockWrapper) (svc *service.LndhubService, err error) { // change this if you want to run tests using sqlite // dbUri := "file:data_test.db" //make sure the datbase is empty every time you run the test suite @@ -51,12 +51,17 @@ func LndHubTestServiceInit() (svc *service.LndhubService, err error) { return nil, fmt.Errorf("failed to migrate: %w", err) } - lndClient, err := lnd.NewLNDclient(lnd.LNDoptions{ - Address: c.LNDAddress, - MacaroonHex: c.LNDMacaroonHex, - }) - if err != nil { - return nil, fmt.Errorf("failed to initialize lnd service client: %w", err) + var lndClient lnd.LightningClientWrapper + if lndClientMock == nil { + lndClient, err = lnd.NewLNDclient(lnd.LNDoptions{ + Address: c.LNDAddress, + MacaroonHex: c.LNDMacaroonHex, + }) + if err != nil { + return nil, fmt.Errorf("failed to initialize lnd service client: %w", err) + } + } else { + lndClient = lndClientMock } logger := lib.Logger(c.LogFilePath) From e67d759797fd95a6686639b68f6b1b03db18d717 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Thu, 17 Feb 2022 20:00:29 +0100 Subject: [PATCH 12/24] Tear down test and suite properly in tests to fix failures --- integration_tests/incoming_payment_test.go | 16 ++++++++++------ integration_tests/internal_payment_test.go | 22 +++++++++++++++------- integration_tests/payment_failure_test.go | 21 +++++++++++++-------- 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/integration_tests/incoming_payment_test.go b/integration_tests/incoming_payment_test.go index 209287d..d579f6e 100644 --- a/integration_tests/incoming_payment_test.go +++ b/integration_tests/incoming_payment_test.go @@ -31,10 +31,11 @@ const ( type IncomingPaymentTestSuite struct { TestSuite - fundingClient *lnd.LNDWrapper - service *service.LndhubService - userLogin controllers.CreateUserResponseBody - userToken string + fundingClient *lnd.LNDWrapper + service *service.LndhubService + userLogin controllers.CreateUserResponseBody + userToken string + invoiceUpdateSubCancelFn context.CancelFunc } func (suite *IncomingPaymentTestSuite) SetupSuite() { @@ -56,7 +57,10 @@ func (suite *IncomingPaymentTestSuite) SetupSuite() { log.Fatalf("Error creating test users: %v", err) } // Subscribe to LND invoice updates in the background - go svc.InvoiceUpdateSubscription(context.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() @@ -70,7 +74,7 @@ func (suite *IncomingPaymentTestSuite) SetupSuite() { } func (suite *IncomingPaymentTestSuite) TearDownSuite() { - + suite.invoiceUpdateSubCancelFn() } func (suite *IncomingPaymentTestSuite) TestIncomingPayment() { diff --git a/integration_tests/internal_payment_test.go b/integration_tests/internal_payment_test.go index 7aec678..6535562 100644 --- a/integration_tests/internal_payment_test.go +++ b/integration_tests/internal_payment_test.go @@ -21,12 +21,13 @@ import ( type PaymentTestSuite struct { TestSuite - fundingClient *lnd.LNDWrapper - service *service.LndhubService - aliceLogin controllers.CreateUserResponseBody - aliceToken string - bobLogin controllers.CreateUserResponseBody - bobToken string + fundingClient *lnd.LNDWrapper + service *service.LndhubService + aliceLogin controllers.CreateUserResponseBody + aliceToken string + bobLogin controllers.CreateUserResponseBody + bobToken string + invoiceUpdateSubCancelFn context.CancelFunc } func (suite *PaymentTestSuite) SetupSuite() { @@ -48,7 +49,10 @@ func (suite *PaymentTestSuite) SetupSuite() { log.Fatalf("Error creating test users: %v", err) } // Subscribe to LND invoice updates in the background - go svc.InvoiceUpdateSubscription(context.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() @@ -68,7 +72,11 @@ func (suite *PaymentTestSuite) SetupSuite() { } func (suite *PaymentTestSuite) TearDownSuite() { + suite.invoiceUpdateSubCancelFn() +} +func (suite *PaymentTestSuite) TearDownTest() { + clearTable(suite.service, "transaction_entries") } func (suite *PaymentTestSuite) TestInternalPayment() { diff --git a/integration_tests/payment_failure_test.go b/integration_tests/payment_failure_test.go index 286b7d4..7e4c5f2 100644 --- a/integration_tests/payment_failure_test.go +++ b/integration_tests/payment_failure_test.go @@ -28,12 +28,13 @@ const ( type PaymentTestErrorsSuite struct { TestSuite - fundingClient *lnd.LNDWrapper - service *service.LndhubService - aliceLogin controllers.CreateUserResponseBody - aliceToken string - bobLogin controllers.CreateUserResponseBody - bobToken string + fundingClient *lnd.LNDWrapper + service *service.LndhubService + aliceLogin controllers.CreateUserResponseBody + aliceToken string + bobLogin controllers.CreateUserResponseBody + bobToken string + invoiceUpdateSubCancelFn context.CancelFunc } func (suite *PaymentTestErrorsSuite) SetupSuite() { @@ -65,7 +66,10 @@ func (suite *PaymentTestErrorsSuite) SetupSuite() { log.Fatalf("Error creating test users: %v", err) } // Subscribe to LND invoice updates in the background - go svc.InvoiceUpdateSubscription(context.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() @@ -137,8 +141,9 @@ func (suite *PaymentTestErrorsSuite) TestExternalFailingInvoice() { // assert that balance is the same assert.Equal(suite.T(), int64(aliceFundingSats), aliceBalance) } -func (suite *PaymentTestErrorsSuite) TearDownSuite() { +func (suite *PaymentTestErrorsSuite) TearDownSuite() { + suite.invoiceUpdateSubCancelFn() } func TestPaymentTestErrorsSuite(t *testing.T) { From 5d0bcfdfa5dca98569dabd3a5475a6b8b46ed895 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Thu, 17 Feb 2022 20:15:32 +0100 Subject: [PATCH 13/24] Add failing internal payment test --- integration_tests/internal_payment_test.go | 56 ++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/integration_tests/internal_payment_test.go b/integration_tests/internal_payment_test.go index 6535562..7a21cd8 100644 --- a/integration_tests/internal_payment_test.go +++ b/integration_tests/internal_payment_test.go @@ -2,10 +2,12 @@ package integration_tests import ( "context" + "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" @@ -77,6 +79,7 @@ func (suite *PaymentTestSuite) TearDownSuite() { func (suite *PaymentTestSuite) TearDownTest() { clearTable(suite.service, "transaction_entries") + clearTable(suite.service, "invoices") } func (suite *PaymentTestSuite) TestInternalPayment() { @@ -107,6 +110,59 @@ func (suite *PaymentTestSuite) TestInternalPayment() { assert.Equal(suite.T(), responses.NotEnoughBalanceError.Code, errorResp.Code) } +func (suite *PaymentTestSuite) TestInternalPaymentFail() { + aliceFundingSats := 1000 + bobSatRequested := 500 + //fund alice account + invoiceResponse := suite.createAddInvoiceReq(aliceFundingSats, "integration test internal payment alice", suite.aliceToken) + sendPaymentRequest := lnrpc.SendRequest{ + PaymentRequest: invoiceResponse.PayReq, + FeeLimit: nil, + } + _, err := suite.fundingClient.SendPaymentSync(context.Background(), &sendPaymentRequest) + assert.NoError(suite.T(), err) + + //wait a bit for the callback event to hit + time.Sleep(100 * time.Millisecond) + + //create invoice for bob + bobInvoice := suite.createAddInvoiceReq(bobSatRequested, "integration test internal payment bob", suite.bobToken) + //pay bob from alice + payResponse := suite.createPayInvoiceReq(bobInvoice.PayReq, suite.aliceToken) + assert.NotEmpty(suite.T(), payResponse.PaymentPreimage) + //try to pay same invoice again for make it fail + _ = suite.createPayInvoiceReqError(bobInvoice.PayReq, suite.aliceToken) + + userId := getUserIdFromToken(suite.aliceToken) + invoices, err := suite.service.InvoicesFor(context.Background(), userId, common.InvoiceTypeOutgoing) + if err != nil { + fmt.Printf("Error when getting invoices %v\n", err.Error()) + } + + // check if first one is settled, but second one error (they are ordered desc by id) + assert.Equal(suite.T(), 2, len(invoices)) + assert.Equal(suite.T(), common.InvoiceStateError, invoices[0].State) + assert.Equal(suite.T(), common.InvoiceStateSettled, invoices[1].State) + transactonEntries, err := suite.service.TransactionEntriesFor(context.Background(), userId) + if err != nil { + fmt.Printf("Error when getting transaction entries %v\n", err.Error()) + } + + aliceBalance, err := suite.service.CurrentUserBalance(context.Background(), userId) + if err != nil { + fmt.Printf("Error when getting balance %v\n", err.Error()) + } + + // check if there are 4 transaction entries, with reversed credit and debit account ids for last 2 + assert.Equal(suite.T(), 4, len(transactonEntries)) + assert.Equal(suite.T(), transactonEntries[2].CreditAccountID, transactonEntries[3].DebitAccountID) + assert.Equal(suite.T(), transactonEntries[2].DebitAccountID, transactonEntries[3].CreditAccountID) + assert.Equal(suite.T(), transactonEntries[2].Amount, int64(bobSatRequested)) + assert.Equal(suite.T(), transactonEntries[3].Amount, int64(bobSatRequested)) + // assert that balance was reduced only once + assert.Equal(suite.T(), int64(bobSatRequested), int64(aliceBalance)) +} + func TestInternalPaymentTestSuite(t *testing.T) { suite.Run(t, new(PaymentTestSuite)) } From 09aee50ec054e3a40012ffa790ea1ef6b64cb60b Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Thu, 17 Feb 2022 20:17:29 +0100 Subject: [PATCH 14/24] Cleanup --- integration_tests/incoming_payment_test.go | 5 ----- integration_tests/outgoing_payment_test.go | 9 --------- integration_tests/payment_failure_test.go | 5 ----- integration_tests/util.go | 11 +++++++++-- 4 files changed, 9 insertions(+), 21 deletions(-) diff --git a/integration_tests/incoming_payment_test.go b/integration_tests/incoming_payment_test.go index d579f6e..0683126 100644 --- a/integration_tests/incoming_payment_test.go +++ b/integration_tests/incoming_payment_test.go @@ -24,11 +24,6 @@ import ( "github.com/stretchr/testify/suite" ) -const ( - lnd2RegtestAddress = "rpc.lnd2.regtest.getalby.com:443" - lnd2RegtestMacaroonHex = "0201036C6E6402F801030A101782922F4358E80655920FC7A7C3E9291201301A160A0761646472657373120472656164120577726974651A130A04696E666F120472656164120577726974651A170A08696E766F69636573120472656164120577726974651A210A086D616361726F6F6E120867656E6572617465120472656164120577726974651A160A076D657373616765120472656164120577726974651A170A086F6666636861696E120472656164120577726974651A160A076F6E636861696E120472656164120577726974651A140A057065657273120472656164120577726974651A180A067369676E6572120867656E657261746512047265616400000620628FFB2938C8540DD3AA5E578D9B43456835FAA176E175FFD4F9FBAE540E3BE9" -) - type IncomingPaymentTestSuite struct { TestSuite fundingClient *lnd.LNDWrapper diff --git a/integration_tests/outgoing_payment_test.go b/integration_tests/outgoing_payment_test.go index 423867c..994a9ff 100644 --- a/integration_tests/outgoing_payment_test.go +++ b/integration_tests/outgoing_payment_test.go @@ -50,12 +50,3 @@ func (suite *PaymentTestSuite) TestOutGoingPayment() { } assert.Equal(suite.T(), 2, len(transactonEntries)) } - -func (suite *PaymentTestSuite) TestOutGoingPaymentFailure() { - //TODO: use a new implementation of LNDClientWrapper interface to test different scenarios - //might be better if this has it's own suite - //because we need a different LND client - // - payment fails directly - // - payment fails after some time, check that balance is locked in the meantime and is restored afterwards - // - payment call succeeds after some time, check that balance is locked in the meantime and is _not_ restored afterwards -} diff --git a/integration_tests/payment_failure_test.go b/integration_tests/payment_failure_test.go index 7e4c5f2..6aba600 100644 --- a/integration_tests/payment_failure_test.go +++ b/integration_tests/payment_failure_test.go @@ -21,11 +21,6 @@ import ( "github.com/stretchr/testify/suite" ) -const ( - lnd1RegtestAddress = "rpc.lnd1.regtest.getalby.com:443" - lnd1RegtestMacaroonHex = "0201036c6e6402f801030a10e2133a1cac2c5b4d56e44e32dc64c8551201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620c4f9783e0873fa50a2091806f5ebb919c5dc432e33800b401463ada6485df0ed" -) - type PaymentTestErrorsSuite struct { TestSuite fundingClient *lnd.LNDWrapper diff --git a/integration_tests/util.go b/integration_tests/util.go index 9adef9b..6999f10 100644 --- a/integration_tests/util.go +++ b/integration_tests/util.go @@ -23,6 +23,13 @@ import ( "github.com/uptrace/bun/migrate" ) +const ( + lnd1RegtestAddress = "rpc.lnd1.regtest.getalby.com:443" + lnd1RegtestMacaroonHex = "0201036c6e6402f801030a10e2133a1cac2c5b4d56e44e32dc64c8551201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620c4f9783e0873fa50a2091806f5ebb919c5dc432e33800b401463ada6485df0ed" + lnd2RegtestAddress = "rpc.lnd2.regtest.getalby.com:443" + lnd2RegtestMacaroonHex = "0201036C6E6402F801030A101782922F4358E80655920FC7A7C3E9291201301A160A0761646472657373120472656164120577726974651A130A04696E666F120472656164120577726974651A170A08696E766F69636573120472656164120577726974651A210A086D616361726F6F6E120867656E6572617465120472656164120577726974651A160A076D657373616765120472656164120577726974651A170A086F6666636861696E120472656164120577726974651A160A076F6E636861696E120472656164120577726974651A140A057065657273120472656164120577726974651A180A067369676E6572120867656E657261746512047265616400000620628FFB2938C8540DD3AA5E578D9B43456835FAA176E175FFD4F9FBAE540E3BE9" +) + func LndHubTestServiceInit(lndClientMock *LNDMockWrapper) (svc *service.LndhubService, err error) { // change this if you want to run tests using sqlite // dbUri := "file:data_test.db" @@ -32,8 +39,8 @@ func LndHubTestServiceInit(lndClientMock *LNDMockWrapper) (svc *service.LndhubSe DatabaseUri: dbUri, JWTSecret: []byte("SECRET"), JWTExpiry: 3600, - LNDAddress: "rpc.lnd1.regtest.getalby.com:443", - LNDMacaroonHex: "0201036c6e6402f801030a10e2133a1cac2c5b4d56e44e32dc64c8551201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620c4f9783e0873fa50a2091806f5ebb919c5dc432e33800b401463ada6485df0ed", + LNDAddress: lnd1RegtestAddress, + LNDMacaroonHex: lnd1RegtestMacaroonHex, } dbConn, err := db.Open(c.DatabaseUri) if err != nil { From 130760bbb1ae6a445fe165824729abeb646fbd8d Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Thu, 17 Feb 2022 20:29:45 +0100 Subject: [PATCH 15/24] Add error message field to invoice --- db/models/invoice.go | 1 + integration_tests/lnd-mock.go | 4 +++- integration_tests/payment_failure_test.go | 1 + lib/service/invoices.go | 10 +++++----- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/db/models/invoice.go b/db/models/invoice.go index ebb0aa5..579bd95 100644 --- a/db/models/invoice.go +++ b/db/models/invoice.go @@ -22,6 +22,7 @@ type Invoice struct { Preimage string `json:"preimage" bun:",nullzero"` Internal bool `json:"internal" bun:",nullzero"` State string `json:"state" bun:",default:'initialized'"` + ErrorMessage string `json:"error_mesage" bun:",default:''"` AddIndex uint64 `json:"add_index" bun:",nullzero"` CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` ExpiresAt bun.NullTime `bun:",nullzero"` diff --git a/integration_tests/lnd-mock.go b/integration_tests/lnd-mock.go index 259b59b..00362bd 100644 --- a/integration_tests/lnd-mock.go +++ b/integration_tests/lnd-mock.go @@ -9,6 +9,8 @@ import ( "google.golang.org/grpc" ) +const SendPaymentMockError = "mocked send payment error" + type LNDMockWrapper struct { *lnd.LNDWrapper } @@ -25,5 +27,5 @@ func NewLNDMockWrapper(lndOptions lnd.LNDoptions) (result *LNDMockWrapper, err e } func (wrapper *LNDMockWrapper) SendPaymentSync(ctx context.Context, req *lnrpc.SendRequest, options ...grpc.CallOption) (*lnrpc.SendResponse, error) { - return nil, errors.New("mocked send payment error") + return nil, errors.New(SendPaymentMockError) } diff --git a/integration_tests/payment_failure_test.go b/integration_tests/payment_failure_test.go index 6aba600..96240f0 100644 --- a/integration_tests/payment_failure_test.go +++ b/integration_tests/payment_failure_test.go @@ -116,6 +116,7 @@ func (suite *PaymentTestErrorsSuite) TestExternalFailingInvoice() { } assert.Equal(suite.T(), 1, len(invoices)) assert.Equal(suite.T(), common.InvoiceStateError, invoices[0].State) + assert.Equal(suite.T(), SendPaymentMockError, invoices[0].ErrorMessage) transactonEntries, err := suite.service.TransactionEntriesFor(context.Background(), userId) if err != nil { diff --git a/lib/service/invoices.go b/lib/service/invoices.go index b1a9182..f382ae1 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -164,8 +164,6 @@ func (svc *LndhubService) PayInvoice(ctx context.Context, invoice *models.Invoic return nil, err } - // TODO: maybe save errors on the invoice? - var paymentResponse SendPaymentResponse // Check the destination pubkey if it is an internal invoice and going to our node // Here we start using context.Background because we want to complete these calls @@ -192,7 +190,7 @@ func (svc *LndhubService) PayInvoice(ctx context.Context, invoice *models.Invoic return &paymentResponse, err } -func (svc *LndhubService) HandleFailedPayment(ctx context.Context, invoice *models.Invoice, entryToRevert models.TransactionEntry, err error) error { +func (svc *LndhubService) HandleFailedPayment(ctx context.Context, invoice *models.Invoice, entryToRevert models.TransactionEntry, failedPaymentError error) error { // add transaction entry with reverted credit/debit account id entry := models.TransactionEntry{ UserID: invoice.UserID, @@ -201,14 +199,16 @@ func (svc *LndhubService) HandleFailedPayment(ctx context.Context, invoice *mode DebitAccountID: entryToRevert.CreditAccountID, Amount: invoice.Amount, } - _, err = svc.DB.NewInsert().Model(&entry).Exec(ctx) + _, err := svc.DB.NewInsert().Model(&entry).Exec(ctx) if err != nil { // TODO: error logging return err } - // TODO: maybe save errors on the invoice? invoice.State = common.InvoiceStateError + if failedPaymentError != nil { + invoice.ErrorMessage = failedPaymentError.Error() + } _, err = svc.DB.NewUpdate().Model(invoice).WherePK().Exec(ctx) // TODO: error logging From fd80bcea5d0a0081522cc870b3f00a519546f84d Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Thu, 17 Feb 2022 20:36:04 +0100 Subject: [PATCH 16/24] Fix error logging todos --- lib/service/invoices.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/service/invoices.go b/lib/service/invoices.go index f382ae1..a169221 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -142,10 +142,12 @@ func (svc *LndhubService) PayInvoice(ctx context.Context, invoice *models.Invoic // Get the user's current and outgoing account for the transaction entry debitAccount, err := svc.AccountFor(ctx, common.AccountTypeCurrent, userId) if err != nil { + svc.Logger.Errorf("Could not find current account user_id:%v", invoice.UserID) return nil, err } creditAccount, err := svc.AccountFor(ctx, common.AccountTypeOutgoing, userId) if err != nil { + svc.Logger.Errorf("Could not find outgoing account user_id:%v", invoice.UserID) return nil, err } @@ -161,6 +163,7 @@ func (svc *LndhubService) PayInvoice(ctx context.Context, invoice *models.Invoic // If the user does not have enough balance this call fails _, err = svc.DB.NewInsert().Model(&entry).Exec(ctx) if err != nil { + svc.Logger.Errorf("Could not insert transaction entry user_id:%v invoice_id:%v", invoice.UserID, invoice.ID) return nil, err } @@ -201,7 +204,7 @@ func (svc *LndhubService) HandleFailedPayment(ctx context.Context, invoice *mode } _, err := svc.DB.NewInsert().Model(&entry).Exec(ctx) if err != nil { - // TODO: error logging + svc.Logger.Errorf("Could not insert transaction entry user_id:%v invoice_id:%v", invoice.UserID, invoice.ID) return err } @@ -211,7 +214,9 @@ func (svc *LndhubService) HandleFailedPayment(ctx context.Context, invoice *mode } _, err = svc.DB.NewUpdate().Model(invoice).WherePK().Exec(ctx) - // TODO: error logging + if err != nil { + svc.Logger.Errorf("Could not update failed payment invoice user_id:%v invoice_id:%v", invoice.UserID, invoice.ID) + } return err } @@ -220,7 +225,9 @@ func (svc *LndhubService) HandleSuccessfulPayment(ctx context.Context, invoice * invoice.SettledAt = schema.NullTime{Time: time.Now()} _, err := svc.DB.NewUpdate().Model(invoice).WherePK().Exec(ctx) - // TODO: error logging + if err != nil { + svc.Logger.Errorf("Could not update sucessful payment invoice user_id:%v invoice_id:%v", invoice.UserID, invoice.ID) + } return err } From 6a32645b59b1d65d40f8bb3ce897a8ba94cca29c Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Fri, 18 Feb 2022 12:39:24 +0100 Subject: [PATCH 17/24] Add missing test --- integration_tests/internal_payment_test.go | 2 +- integration_tests/lnd-mock.go | 27 +++ integration_tests/outgoing_payment_test.go | 2 +- .../payment_failure_async_test.go | 157 ++++++++++++++++++ integration_tests/payment_failure_test.go | 36 ++-- integration_tests/util.go | 17 +- 6 files changed, 218 insertions(+), 23 deletions(-) create mode 100644 integration_tests/payment_failure_async_test.go diff --git a/integration_tests/internal_payment_test.go b/integration_tests/internal_payment_test.go index 7a21cd8..fc3ff08 100644 --- a/integration_tests/internal_payment_test.go +++ b/integration_tests/internal_payment_test.go @@ -160,7 +160,7 @@ func (suite *PaymentTestSuite) TestInternalPaymentFail() { assert.Equal(suite.T(), transactonEntries[2].Amount, int64(bobSatRequested)) assert.Equal(suite.T(), transactonEntries[3].Amount, int64(bobSatRequested)) // assert that balance was reduced only once - assert.Equal(suite.T(), int64(bobSatRequested), int64(aliceBalance)) + assert.Equal(suite.T(), int64(aliceFundingSats)-int64(bobSatRequested), int64(aliceBalance)) } func TestInternalPaymentTestSuite(t *testing.T) { diff --git a/integration_tests/lnd-mock.go b/integration_tests/lnd-mock.go index 00362bd..a650052 100644 --- a/integration_tests/lnd-mock.go +++ b/integration_tests/lnd-mock.go @@ -29,3 +29,30 @@ func NewLNDMockWrapper(lndOptions lnd.LNDoptions) (result *LNDMockWrapper, err e func (wrapper *LNDMockWrapper) SendPaymentSync(ctx context.Context, req *lnrpc.SendRequest, options ...grpc.CallOption) (*lnrpc.SendResponse, error) { return nil, errors.New(SendPaymentMockError) } + +// mock where send payment sync failure is controlled by channel +var errorMessageChannel = make(chan string, 1) + +type LNDMockWrapperAsync struct { + *lnd.LNDWrapper +} + +func NewLNDMockWrapperAsync(lndOptions lnd.LNDoptions) (result *LNDMockWrapperAsync, err error) { + lnd, err := lnd.NewLNDclient(lndOptions) + if err != nil { + return nil, err + } + + return &LNDMockWrapperAsync{ + LNDWrapper: lnd, + }, nil +} + +func (wrapper *LNDMockWrapperAsync) SendPaymentSync(ctx context.Context, req *lnrpc.SendRequest, options ...grpc.CallOption) (*lnrpc.SendResponse, error) { + errorMessage := <-errorMessageChannel + return nil, errors.New(errorMessage) +} + +func (wrapper *LNDMockWrapperAsync) FailPayment(message string) { + errorMessageChannel <- message +} diff --git a/integration_tests/outgoing_payment_test.go b/integration_tests/outgoing_payment_test.go index 994a9ff..1a8ec20 100644 --- a/integration_tests/outgoing_payment_test.go +++ b/integration_tests/outgoing_payment_test.go @@ -41,7 +41,7 @@ func (suite *PaymentTestSuite) TestOutGoingPayment() { if err != nil { fmt.Printf("Error when getting balance %v\n", err.Error()) } - assert.Equal(suite.T(), int64(500), aliceBalance) + assert.Equal(suite.T(), int64(aliceFundingSats)-int64(externalSatRequested), aliceBalance) // check that no additional transaction entry was created transactonEntries, err := suite.service.TransactionEntriesFor(context.Background(), userId) diff --git a/integration_tests/payment_failure_async_test.go b/integration_tests/payment_failure_async_test.go new file mode 100644 index 0000000..9a23675 --- /dev/null +++ b/integration_tests/payment_failure_async_test.go @@ -0,0 +1,157 @@ +package integration_tests + +import ( + "context" + "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/getAlby/lndhub.go/lnd" + "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 PaymentTestAsyncErrorsSuite struct { + TestSuite + fundingClient *lnd.LNDWrapper + service *service.LndhubService + userLogin controllers.CreateUserResponseBody + userToken string + invoiceUpdateSubCancelFn context.CancelFunc + serviceClient *LNDMockWrapperAsync +} + +func (suite *PaymentTestAsyncErrorsSuite) SetupSuite() { + // use real client for funding only + fundingClient, err := lnd.NewLNDclient(lnd.LNDoptions{ + Address: lnd2RegtestAddress, + MacaroonHex: lnd2RegtestMacaroonHex, + }) + if err != nil { + log.Fatalf("Error setting up funding client: %v", err) + } + + // inject fake lnd client with failing send payment sync into service + lndClient, err := NewLNDMockWrapperAsync(lnd.LNDoptions{ + Address: lnd1RegtestAddress, + MacaroonHex: lnd1RegtestMacaroonHex, + }) + suite.serviceClient = lndClient + if err != nil { + log.Fatalf("Error setting up test client: %v", err) + } + suite.fundingClient = fundingClient + + 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 *PaymentTestAsyncErrorsSuite) TestExternalAsyncFailingInvoice() { + userFundingSats := 1000 + externalSatRequested := 500 + // fund user account + invoiceResponse := suite.createAddInvoiceReq(userFundingSats, "integration test external payment user", suite.userToken) + sendPaymentRequest := lnrpc.SendRequest{ + PaymentRequest: invoiceResponse.PayReq, + FeeLimit: nil, + } + _, err := suite.fundingClient.SendPaymentSync(context.Background(), &sendPaymentRequest) + assert.NoError(suite.T(), err) + + // wait a bit for the callback event to hit + time.Sleep(100 * time.Millisecond) + + // create external invoice + externalInvoice := lnrpc.Invoice{ + Memo: "integration tests: external pay from user", + Value: int64(externalSatRequested), + } + invoice, err := suite.fundingClient.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 request to fail + 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) + + // fail payment and wait a bit + suite.serviceClient.FailPayment(SendPaymentMockError) + time.Sleep(2 * 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) + assert.Equal(suite.T(), SendPaymentMockError, 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)) +} + +func (suite *PaymentTestAsyncErrorsSuite) TearDownSuite() { + suite.invoiceUpdateSubCancelFn() +} + +func TestPaymentTestErrorsAsyncSuite(t *testing.T) { + suite.Run(t, new(PaymentTestAsyncErrorsSuite)) +} diff --git a/integration_tests/payment_failure_test.go b/integration_tests/payment_failure_test.go index 96240f0..23ed8b5 100644 --- a/integration_tests/payment_failure_test.go +++ b/integration_tests/payment_failure_test.go @@ -25,10 +25,8 @@ type PaymentTestErrorsSuite struct { TestSuite fundingClient *lnd.LNDWrapper service *service.LndhubService - aliceLogin controllers.CreateUserResponseBody - aliceToken string - bobLogin controllers.CreateUserResponseBody - bobToken string + userLogin controllers.CreateUserResponseBody + userToken string invoiceUpdateSubCancelFn context.CancelFunc } @@ -56,7 +54,7 @@ func (suite *PaymentTestErrorsSuite) SetupSuite() { if err != nil { log.Fatalf("Error initializing test service: %v", err) } - users, userTokens, err := createUsers(svc, 2) + users, userTokens, err := createUsers(svc, 1) if err != nil { log.Fatalf("Error creating test users: %v", err) } @@ -71,12 +69,10 @@ func (suite *PaymentTestErrorsSuite) SetupSuite() { e.HTTPErrorHandler = responses.HTTPErrorHandler e.Validator = &lib.CustomValidator{Validator: validator.New()} suite.echo = e - assert.Equal(suite.T(), 2, len(users)) - assert.Equal(suite.T(), 2, len(userTokens)) - suite.aliceLogin = users[0] - suite.aliceToken = userTokens[0] - suite.bobLogin = users[1] - suite.bobToken = userTokens[1] + 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) @@ -84,10 +80,10 @@ func (suite *PaymentTestErrorsSuite) SetupSuite() { } func (suite *PaymentTestErrorsSuite) TestExternalFailingInvoice() { - aliceFundingSats := 1000 + userFundingSats := 1000 externalSatRequested := 500 - //fund alice account - invoiceResponse := suite.createAddInvoiceReq(aliceFundingSats, "integration test external payment alice", suite.aliceToken) + //fund user account + invoiceResponse := suite.createAddInvoiceReq(userFundingSats, "integration test external payment user", suite.userToken) sendPaymentRequest := lnrpc.SendRequest{ PaymentRequest: invoiceResponse.PayReq, FeeLimit: nil, @@ -100,15 +96,15 @@ func (suite *PaymentTestErrorsSuite) TestExternalFailingInvoice() { //create external invoice externalInvoice := lnrpc.Invoice{ - Memo: "integration tests: external pay from alice", + Memo: "integration tests: external pay from user", Value: int64(externalSatRequested), } invoice, err := suite.fundingClient.AddInvoice(context.Background(), &externalInvoice) assert.NoError(suite.T(), err) - //pay external from alice, mock will fail - _ = suite.createPayInvoiceReqError(invoice.PaymentRequest, suite.aliceToken) + //pay external from user, mock will fail immediately + _ = suite.createPayInvoiceReqError(invoice.PaymentRequest, suite.userToken) - userId := getUserIdFromToken(suite.aliceToken) + userId := getUserIdFromToken(suite.userToken) invoices, err := suite.service.InvoicesFor(context.Background(), userId, common.InvoiceTypeOutgoing) if err != nil { @@ -123,7 +119,7 @@ func (suite *PaymentTestErrorsSuite) TestExternalFailingInvoice() { fmt.Printf("Error when getting transaction entries %v\n", err.Error()) } - aliceBalance, err := suite.service.CurrentUserBalance(context.Background(), userId) + userBalance, err := suite.service.CurrentUserBalance(context.Background(), userId) if err != nil { fmt.Printf("Error when getting balance %v\n", err.Error()) } @@ -135,7 +131,7 @@ func (suite *PaymentTestErrorsSuite) TestExternalFailingInvoice() { assert.Equal(suite.T(), transactonEntries[1].Amount, int64(externalSatRequested)) assert.Equal(suite.T(), transactonEntries[2].Amount, int64(externalSatRequested)) // assert that balance is the same - assert.Equal(suite.T(), int64(aliceFundingSats), aliceBalance) + assert.Equal(suite.T(), int64(userFundingSats), userBalance) } func (suite *PaymentTestErrorsSuite) TearDownSuite() { diff --git a/integration_tests/util.go b/integration_tests/util.go index 6999f10..5f0a443 100644 --- a/integration_tests/util.go +++ b/integration_tests/util.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "time" "github.com/getAlby/lndhub.go/controllers" "github.com/getAlby/lndhub.go/db" @@ -30,7 +31,7 @@ const ( lnd2RegtestMacaroonHex = "0201036C6E6402F801030A101782922F4358E80655920FC7A7C3E9291201301A160A0761646472657373120472656164120577726974651A130A04696E666F120472656164120577726974651A170A08696E766F69636573120472656164120577726974651A210A086D616361726F6F6E120867656E6572617465120472656164120577726974651A160A076D657373616765120472656164120577726974651A170A086F6666636861696E120472656164120577726974651A160A076F6E636861696E120472656164120577726974651A140A057065657273120472656164120577726974651A180A067369676E6572120867656E657261746512047265616400000620628FFB2938C8540DD3AA5E578D9B43456835FAA176E175FFD4F9FBAE540E3BE9" ) -func LndHubTestServiceInit(lndClientMock *LNDMockWrapper) (svc *service.LndhubService, err error) { +func LndHubTestServiceInit(lndClientMock lnd.LightningClientWrapper) (svc *service.LndhubService, err error) { // change this if you want to run tests using sqlite // dbUri := "file:data_test.db" //make sure the datbase is empty every time you run the test suite @@ -181,3 +182,17 @@ func (suite *TestSuite) createPayInvoiceReqError(payReq string, token string) *r assert.NoError(suite.T(), json.NewDecoder(rec.Body).Decode(errorResponse)) return errorResponse } + +func (suite *TestSuite) createPayInvoiceReqWithCancel(payReq string, token string) { + rec := httptest.NewRecorder() + var buf bytes.Buffer + assert.NoError(suite.T(), json.NewEncoder(&buf).Encode(&controllers.PayInvoiceRequestBody{ + Invoice: payReq, + })) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + req := httptest.NewRequest(http.MethodPost, "/payinvoice", &buf).WithContext(ctx) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + suite.echo.ServeHTTP(rec, req) +} From a071b3bd1a9357fe8366222108aaff312c61a3a3 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Fri, 18 Feb 2022 12:46:06 +0100 Subject: [PATCH 18/24] Fix error message bun struct tag --- db/models/invoice.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/models/invoice.go b/db/models/invoice.go index 579bd95..cba465c 100644 --- a/db/models/invoice.go +++ b/db/models/invoice.go @@ -22,7 +22,7 @@ type Invoice struct { Preimage string `json:"preimage" bun:",nullzero"` Internal bool `json:"internal" bun:",nullzero"` State string `json:"state" bun:",default:'initialized'"` - ErrorMessage string `json:"error_mesage" bun:",default:''"` + ErrorMessage string `json:"error_mesage" bun:",nullzero"` AddIndex uint64 `json:"add_index" bun:",nullzero"` CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` ExpiresAt bun.NullTime `bun:",nullzero"` From 596dbc6ecaff73d0ec07220ff51745880cd77b4e Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Fri, 18 Feb 2022 12:46:35 +0100 Subject: [PATCH 19/24] Use sentry capture for handle payment errors --- lib/service/invoices.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/service/invoices.go b/lib/service/invoices.go index a169221..9b43d93 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -9,6 +9,7 @@ import ( "github.com/getAlby/lndhub.go/common" "github.com/getAlby/lndhub.go/db/models" + "github.com/getsentry/sentry-go" "github.com/labstack/gommon/random" "github.com/lightningnetwork/lnd/lnrpc" "github.com/uptrace/bun" @@ -204,6 +205,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) return err } @@ -215,6 +217,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) } return err @@ -226,6 +229,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) } return err From f5b58e66599c50b6c1bda38ea2305c7af8dd7ceb Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Fri, 18 Feb 2022 14:07:15 +0100 Subject: [PATCH 20/24] Add comment to explain async mock wrapper --- integration_tests/lnd-mock.go | 1 + 1 file changed, 1 insertion(+) diff --git a/integration_tests/lnd-mock.go b/integration_tests/lnd-mock.go index a650052..ee2cb24 100644 --- a/integration_tests/lnd-mock.go +++ b/integration_tests/lnd-mock.go @@ -31,6 +31,7 @@ func (wrapper *LNDMockWrapper) SendPaymentSync(ctx context.Context, req *lnrpc.S } // mock where send payment sync failure is controlled by channel +// even though send payment method is still sync, suffix "Async" here is used to show intention of using this mock var errorMessageChannel = make(chan string, 1) type LNDMockWrapperAsync struct { From b70578eaf94777bdeab772fd7dbfa1d1845fa9c0 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Fri, 18 Feb 2022 14:30:51 +0100 Subject: [PATCH 21/24] Add check payment tests --- controllers/checkpayment.ctrl.go | 8 +- integration_tests/checkpayment_test.go | 132 +++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 integration_tests/checkpayment_test.go diff --git a/controllers/checkpayment.ctrl.go b/controllers/checkpayment.ctrl.go index d2068a1..c280058 100644 --- a/controllers/checkpayment.ctrl.go +++ b/controllers/checkpayment.ctrl.go @@ -13,6 +13,10 @@ type CheckPaymentController struct { svc *service.LndhubService } +type CheckPaymentResponseBody struct { + IsPaid bool `json:"paid"` +} + func NewCheckPaymentController(svc *service.LndhubService) *CheckPaymentController { return &CheckPaymentController{svc: svc} } @@ -30,9 +34,7 @@ func (controller *CheckPaymentController) CheckPayment(c echo.Context) error { return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) } - var responseBody struct { - IsPaid bool `json:"paid"` - } + responseBody := &CheckPaymentResponseBody{} responseBody.IsPaid = !invoice.SettledAt.IsZero() return c.JSON(http.StatusOK, &responseBody) } diff --git a/integration_tests/checkpayment_test.go b/integration_tests/checkpayment_test.go new file mode 100644 index 0000000..fade5e7 --- /dev/null +++ b/integration_tests/checkpayment_test.go @@ -0,0 +1,132 @@ +package integration_tests + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "net/http/httptest" + "testing" + "time" + + "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/getAlby/lndhub.go/lnd" + "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 CheckPaymentTestSuite struct { + TestSuite + fundingClient *lnd.LNDWrapper + service *service.LndhubService + userLogin controllers.CreateUserResponseBody + userToken string + invoiceUpdateSubCancelFn context.CancelFunc +} + +func (suite *CheckPaymentTestSuite) SetupSuite() { + lndClient, err := lnd.NewLNDclient(lnd.LNDoptions{ + Address: lnd2RegtestAddress, + MacaroonHex: lnd2RegtestMacaroonHex, + }) + if err != nil { + log.Fatalf("Error setting up funding client: %v", err) + } + suite.fundingClient = lndClient + + svc, err := LndHubTestServiceInit(nil) + 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.POST("/addinvoice", controllers.NewAddInvoiceController(suite.service).AddInvoice) + suite.echo.POST("/payinvoice", controllers.NewPayInvoiceController(suite.service).PayInvoice) + suite.echo.GET("/checkpayment/:payment_hash", controllers.NewCheckPaymentController(suite.service).CheckPayment) +} + +func (suite *CheckPaymentTestSuite) TestCheckPaymentNotFound() { + dummyRHash := "12345" + req := httptest.NewRequest(http.MethodGet, "/checkpayment/"+dummyRHash, nil) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", suite.userToken)) + rec := httptest.NewRecorder() + suite.echo.ServeHTTP(rec, req) + errorResponse := &responses.ErrorResponse{} + assert.Equal(suite.T(), http.StatusBadRequest, rec.Code) + assert.NoError(suite.T(), json.NewDecoder(rec.Body).Decode(errorResponse)) +} + +func (suite *CheckPaymentTestSuite) TestCheckPaymentProperIsPaidResponse() { + // create incoming invoice and fund account + invoice := suite.createAddInvoiceReq(1000, "integration test check payments for user", suite.userToken) + sendPaymentRequest := lnrpc.SendRequest{ + PaymentRequest: invoice.PayReq, + FeeLimit: nil, + } + _, err := suite.fundingClient.SendPaymentSync(context.Background(), &sendPaymentRequest) + assert.NoError(suite.T(), err) + + // wait a bit for the callback event to hit + time.Sleep(100 * time.Millisecond) + // create invoice + invoice = suite.createAddInvoiceReq(500, "integration test check payments for user", suite.userToken) + // pay invoice, this will create outgoing invoice and settle it + + // check payment not paid + req := httptest.NewRequest(http.MethodGet, "/checkpayment/"+invoice.RHash, nil) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", suite.userToken)) + rec := httptest.NewRecorder() + suite.echo.ServeHTTP(rec, req) + checkPaymentResponse := &controllers.CheckPaymentResponseBody{} + assert.Equal(suite.T(), http.StatusOK, rec.Code) + assert.NoError(suite.T(), json.NewDecoder(rec.Body).Decode(checkPaymentResponse)) + assert.False(suite.T(), checkPaymentResponse.IsPaid) + + // pay external from user + payResponse := suite.createPayInvoiceReq(invoice.PaymentRequest, suite.userToken) + assert.NotEmpty(suite.T(), payResponse.PaymentPreimage) + + // check payment is paid + req = httptest.NewRequest(http.MethodGet, "/checkpayment/"+invoice.RHash, nil) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", suite.userToken)) + rec = httptest.NewRecorder() + suite.echo.ServeHTTP(rec, req) + checkPaymentResponse = &controllers.CheckPaymentResponseBody{} + assert.Equal(suite.T(), http.StatusOK, rec.Code) + assert.NoError(suite.T(), json.NewDecoder(rec.Body).Decode(checkPaymentResponse)) + assert.True(suite.T(), checkPaymentResponse.IsPaid) +} + +func (suite *CheckPaymentTestSuite) TearDownSuite() { + suite.invoiceUpdateSubCancelFn() +} + +func TestCheckPaymentSuite(t *testing.T) { + suite.Run(t, new(CheckPaymentTestSuite)) +} From 0671e386976b2d92aada5bb9c6705b19ba886230 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Fri, 18 Feb 2022 14:30:57 +0100 Subject: [PATCH 22/24] Add get info tests --- integration_tests/getinfo_test.go | 83 +++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 integration_tests/getinfo_test.go diff --git a/integration_tests/getinfo_test.go b/integration_tests/getinfo_test.go new file mode 100644 index 0000000..0bb9300 --- /dev/null +++ b/integration_tests/getinfo_test.go @@ -0,0 +1,83 @@ +package integration_tests + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "net/http/httptest" + "testing" + + "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 GetInfoTestSuite struct { + TestSuite + service *service.LndhubService + userLogin controllers.CreateUserResponseBody + userToken string +} + +func (suite *GetInfoTestSuite) SetupSuite() { + svc, err := LndHubTestServiceInit(nil) + 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) + } + + 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("/getinfo", controllers.NewGetInfoController(svc).GetInfo) +} + +func (suite *GetInfoTestSuite) TestGetInfoWithDefaultAlias() { + req := httptest.NewRequest(http.MethodGet, "/getinfo", nil) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", suite.userToken)) + rec := httptest.NewRecorder() + suite.echo.ServeHTTP(rec, req) + getInfoResponse := &lnrpc.GetInfoResponse{} + assert.Equal(suite.T(), http.StatusOK, rec.Code) + assert.NoError(suite.T(), json.NewDecoder(rec.Body).Decode(getInfoResponse)) + assert.NotNil(suite.T(), getInfoResponse) + assert.Equal(suite.T(), "alby-simnet-lnd1", getInfoResponse.Alias) +} + +func (suite *GetInfoTestSuite) TestGetInfoWithGivenAlias() { + suite.service.Config.CustomName = "test-alias" + req := httptest.NewRequest(http.MethodGet, "/getinfo", nil) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", suite.userToken)) + rec := httptest.NewRecorder() + suite.echo.ServeHTTP(rec, req) + getInfoResponse := &lnrpc.GetInfoResponse{} + assert.Equal(suite.T(), http.StatusOK, rec.Code) + assert.NoError(suite.T(), json.NewDecoder(rec.Body).Decode(getInfoResponse)) + assert.NotNil(suite.T(), getInfoResponse) + assert.Equal(suite.T(), suite.service.Config.CustomName, getInfoResponse.Alias) +} + +func (suite *GetInfoTestSuite) TearDownSuite() {} + +func TestGetInfoSuite(t *testing.T) { + suite.Run(t, new(GetInfoTestSuite)) +} From 4a3c93bb17f2b573f607e11ebd8fa1e022646824 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Fri, 18 Feb 2022 16:42:42 +0200 Subject: [PATCH 23/24] Fix typo --- db/models/invoice.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/models/invoice.go b/db/models/invoice.go index cba465c..a117adf 100644 --- a/db/models/invoice.go +++ b/db/models/invoice.go @@ -22,7 +22,7 @@ type Invoice struct { Preimage string `json:"preimage" bun:",nullzero"` Internal bool `json:"internal" bun:",nullzero"` State string `json:"state" bun:",default:'initialized'"` - ErrorMessage string `json:"error_mesage" bun:",nullzero"` + ErrorMessage string `json:"error_message" bun:",nullzero"` AddIndex uint64 `json:"add_index" bun:",nullzero"` CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` ExpiresAt bun.NullTime `bun:",nullzero"` From 18ddf6cacea79be8bf5a8c4aeecc1ba845dceab5 Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Fri, 18 Feb 2022 18:02:10 +0100 Subject: [PATCH 24/24] update README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c54a947..52780c4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # LndHub.go Wrapper for Lightning Network Daemon (lnd). It provides separate accounts with minimum trust for end users. +Live deployment at [ln.getalby.com](https://ln.getalby.com). + ### [LndHub](https://github.com/BlueWallet/LndHub) compatible API implemented in Go using relational database backends * Using a relational database (PostgreSQL and SQLite) @@ -13,7 +15,6 @@ Wrapper for Lightning Network Daemon (lnd). It provides separate accounts with m ## Known Issues * Currently no fee handling (users are currently not charged for lightning transaction fees) -* No handling of in-transit payments ## Configuration