From 4a3e28c9096ea29957e21da35540611b877df517 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Wed, 20 Sep 2023 16:49:32 +0530 Subject: [PATCH 01/19] chore: add custom records to invoice before failure --- controllers/keysend.ctrl.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/controllers/keysend.ctrl.go b/controllers/keysend.ctrl.go index 8d80371..586111c 100644 --- a/controllers/keysend.ctrl.go +++ b/controllers/keysend.ctrl.go @@ -93,10 +93,6 @@ func (controller *KeySendController) KeySend(c echo.Context) error { if err != nil { return err } - if _, err := hex.DecodeString(invoice.DestinationPubkeyHex); err != nil || len(invoice.DestinationPubkeyHex) != common.DestinationPubkeyHexSize { - c.Logger().Errorf("Invalid destination pubkey hex user_id:%v pubkey:%v", userID, len(invoice.DestinationPubkeyHex)) - return c.JSON(http.StatusBadRequest, responses.InvalidDestinationError) - } invoice.DestinationCustomRecords = map[uint64][]byte{} for key, value := range reqBody.CustomRecords { intKey, err := strconv.Atoi(key) @@ -105,6 +101,10 @@ func (controller *KeySendController) KeySend(c echo.Context) error { } invoice.DestinationCustomRecords[uint64(intKey)] = []byte(value) } + if _, err := hex.DecodeString(invoice.DestinationPubkeyHex); err != nil || len(invoice.DestinationPubkeyHex) != common.DestinationPubkeyHexSize { + c.Logger().Errorf("Invalid destination pubkey hex user_id:%v pubkey:%v", userID, len(invoice.DestinationPubkeyHex)) + return c.JSON(http.StatusBadRequest, responses.InvalidDestinationError) + } sendPaymentResponse, err := controller.svc.PayInvoice(c.Request().Context(), invoice) if err != nil { c.Logger().Errorf("Payment failed: user_id:%v error: %v", userID, err) From afbbca8b452c5a65f5d252abb0248e4d9c01dd63 Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Mon, 25 Sep 2023 10:04:34 +0200 Subject: [PATCH 02/19] refactor balance check --- controllers/keysend.ctrl.go | 4 ++-- controllers/payinvoice.ctrl.go | 4 ++-- controllers_v2/keysend.ctrl.go | 4 ++-- controllers_v2/payinvoice.ctrl.go | 4 ++-- lib/responses/errors.go | 7 +++++++ lib/service/user.go | 10 +++++++--- 6 files changed, 22 insertions(+), 11 deletions(-) diff --git a/controllers/keysend.ctrl.go b/controllers/keysend.ctrl.go index 1ae22a9..9ce3d52 100644 --- a/controllers/keysend.ctrl.go +++ b/controllers/keysend.ctrl.go @@ -82,7 +82,7 @@ func (controller *KeySendController) KeySend(c echo.Context) error { }) } - ok, err := controller.svc.BalanceCheck(c.Request().Context(), lnPayReq, userID) + resp, err := controller.svc.CheckPaymentAllowed(c.Request().Context(), lnPayReq, userID) if err != nil { c.Logger().Errorj( log.JSON{ @@ -93,7 +93,7 @@ func (controller *KeySendController) KeySend(c echo.Context) error { ) return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) } - if !ok { + if resp != nil { c.Logger().Errorf("User does not have enough balance user_id:%v amount:%v", userID, lnPayReq.PayReq.NumSatoshis) return c.JSON(http.StatusBadRequest, responses.NotEnoughBalanceError) } diff --git a/controllers/payinvoice.ctrl.go b/controllers/payinvoice.ctrl.go index 69bc67e..2a67c72 100644 --- a/controllers/payinvoice.ctrl.go +++ b/controllers/payinvoice.ctrl.go @@ -97,7 +97,7 @@ func (controller *PayInvoiceController) PayInvoice(c echo.Context) error { } } - ok, err := controller.svc.BalanceCheck(c.Request().Context(), lnPayReq, userID) + resp, err := controller.svc.CheckPaymentAllowed(c.Request().Context(), lnPayReq, userID) if err != nil { c.Logger().Errorj( log.JSON{ @@ -108,7 +108,7 @@ func (controller *PayInvoiceController) PayInvoice(c echo.Context) error { ) return c.JSON(http.StatusBadRequest, responses.GeneralServerError) } - if !ok { + if resp != nil { c.Logger().Errorf("User does not have enough balance user_id:%v amount:%v", userID, lnPayReq.PayReq.NumSatoshis) return c.JSON(http.StatusBadRequest, responses.NotEnoughBalanceError) } diff --git a/controllers_v2/keysend.ctrl.go b/controllers_v2/keysend.ctrl.go index b2f4524..fd8229d 100644 --- a/controllers_v2/keysend.ctrl.go +++ b/controllers_v2/keysend.ctrl.go @@ -171,12 +171,12 @@ func (controller *KeySendController) SingleKeySend(c echo.Context, reqBody *KeyS HttpStatusCode: 400, } } - ok, err := controller.svc.BalanceCheck(c.Request().Context(), lnPayReq, userID) + resp, err := controller.svc.CheckPaymentAllowed(c.Request().Context(), lnPayReq, userID) if err != nil { controller.svc.Logger.Error(err) return nil, &responses.GeneralServerError } - if !ok { + if resp != nil { c.Logger().Errorf("User does not have enough balance user_id:%v amount:%v", userID, lnPayReq.PayReq.NumSatoshis) return nil, &responses.NotEnoughBalanceError } diff --git a/controllers_v2/payinvoice.ctrl.go b/controllers_v2/payinvoice.ctrl.go index 1724e81..473eff5 100644 --- a/controllers_v2/payinvoice.ctrl.go +++ b/controllers_v2/payinvoice.ctrl.go @@ -98,7 +98,7 @@ func (controller *PayInvoiceController) PayInvoice(c echo.Context) error { } lnPayReq.PayReq.NumSatoshis = amt } - ok, err := controller.svc.BalanceCheck(c.Request().Context(), lnPayReq, userID) + resp, err := controller.svc.CheckPaymentAllowed(c.Request().Context(), lnPayReq, userID) if err != nil { c.Logger().Errorj( log.JSON{ @@ -109,7 +109,7 @@ func (controller *PayInvoiceController) PayInvoice(c echo.Context) error { ) return err } - if !ok { + if resp != nil { c.Logger().Errorf("User does not have enough balance user_id:%v amount:%v", userID, lnPayReq.PayReq.NumSatoshis) return c.JSON(http.StatusInternalServerError, responses.NotEnoughBalanceError) } diff --git a/lib/responses/errors.go b/lib/responses/errors.go index 5a293fd..d8126ea 100644 --- a/lib/responses/errors.go +++ b/lib/responses/errors.go @@ -64,6 +64,13 @@ var NotEnoughBalanceError = ErrorResponse{ HttpStatusCode: 400, } +var TooMuchVolumeError = ErrorResponse{ + Error: true, + Code: 2, + Message: "transaction volume too high. please contact support for further assistance.", + HttpStatusCode: 400, +} + var AccountDeactivatedError = ErrorResponse{ Error: true, Code: 1, diff --git a/lib/service/user.go b/lib/service/user.go index a1e9a75..4614595 100644 --- a/lib/service/user.go +++ b/lib/service/user.go @@ -8,6 +8,7 @@ import ( "github.com/getAlby/lndhub.go/common" "github.com/getAlby/lndhub.go/db/models" + "github.com/getAlby/lndhub.go/lib/responses" "github.com/getAlby/lndhub.go/lib/security" "github.com/getAlby/lndhub.go/lnd" "github.com/uptrace/bun" @@ -121,17 +122,20 @@ func (svc *LndhubService) FindUserByLogin(ctx context.Context, login string) (*m return &user, nil } -func (svc *LndhubService) BalanceCheck(ctx context.Context, lnpayReq *lnd.LNPayReq, userId int64) (ok bool, err error) { +func (svc *LndhubService) CheckPaymentAllowed(ctx context.Context, lnpayReq *lnd.LNPayReq, userId int64) (result *responses.ErrorResponse, err error) { currentBalance, err := svc.CurrentUserBalance(ctx, userId) if err != nil { - return false, err + return nil, err } minimumBalance := lnpayReq.PayReq.NumSatoshis if svc.Config.FeeReserve { minimumBalance += svc.CalcFeeLimit(lnpayReq.PayReq.Destination, lnpayReq.PayReq.NumSatoshis) } - return currentBalance >= minimumBalance, nil + if currentBalance >= minimumBalance { + return &responses.NotEnoughBalanceError, nil + } + return nil, nil } func (svc *LndhubService) CalcFeeLimit(destination string, amount int64) int64 { From d6cd58f8afe50e33729f9372ff449eed5c7abc01 Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Mon, 25 Sep 2023 10:24:38 +0200 Subject: [PATCH 03/19] implement volume check --- lib/service/config.go | 2 ++ lib/service/user.go | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/lib/service/config.go b/lib/service/config.go index 1ceb911..ad9efd5 100644 --- a/lib/service/config.go +++ b/lib/service/config.go @@ -37,6 +37,8 @@ type Config struct { MaxSendAmount int64 `envconfig:"MAX_SEND_AMOUNT" default:"0"` MaxAccountBalance int64 `envconfig:"MAX_ACCOUNT_BALANCE" default:"0"` MaxFeeAmount int64 `envconfig:"MAX_FEE_AMOUNT" default:"5000"` + MaxVolume int64 `envconfig:"MAX_VOLUME" default:"5000000"` + MaxVolumePeriod int64 `envconfig:"MAX_VOLUME_PERIOD" default:"2592000"` //in seconds, default 1 month RabbitMQUri string `envconfig:"RABBITMQ_URI"` RabbitMQLndhubInvoiceExchange string `envconfig:"RABBITMQ_INVOICE_EXCHANGE" default:"lndhub_invoice"` RabbitMQLndInvoiceExchange string `envconfig:"RABBITMQ_LND_INVOICE_EXCHANGE" default:"lnd_invoice"` diff --git a/lib/service/user.go b/lib/service/user.go index 4614595..c886b02 100644 --- a/lib/service/user.go +++ b/lib/service/user.go @@ -5,6 +5,7 @@ import ( "database/sql" "fmt" "math" + "time" "github.com/getAlby/lndhub.go/common" "github.com/getAlby/lndhub.go/db/models" @@ -135,6 +136,13 @@ func (svc *LndhubService) CheckPaymentAllowed(ctx context.Context, lnpayReq *lnd if currentBalance >= minimumBalance { return &responses.NotEnoughBalanceError, nil } + volume, err := svc.GetVolumeOverPeriod(ctx, userId, time.Duration(svc.Config.MaxVolumePeriod*int64(time.Second))) + if err != nil { + return nil, err + } + if volume > svc.Config.MaxVolume { + return &responses.TooMuchVolumeError, nil + } return nil, nil } @@ -189,3 +197,17 @@ func (svc *LndhubService) InvoicesFor(ctx context.Context, userId int64, invoice } return invoices, nil } + +func (svc *LndhubService) GetVolumeOverPeriod(ctx context.Context, userId int64, period time.Duration) (result int64, err error) { + + err = svc.DB.NewSelect().Table("invoices"). + ColumnExpr("sum(invoices.amount) as result"). + Where("invoices.user_id = ?", userId). + Where("invoices.state = ?", common.InvoiceStateSettled). + Where("invoices.settled_at >= ?", time.Now().Add(-1*period)). + Scan(ctx, &result) + if err != nil { + return 0, err + } + return result, nil +} From a043f26b1d3b946d8a3c6de0a60ccc0b36e72cbc Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Mon, 25 Sep 2023 10:51:20 +0200 Subject: [PATCH 04/19] fix bug in allowed check --- lib/service/user.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/service/user.go b/lib/service/user.go index c886b02..0e27d77 100644 --- a/lib/service/user.go +++ b/lib/service/user.go @@ -133,7 +133,7 @@ func (svc *LndhubService) CheckPaymentAllowed(ctx context.Context, lnpayReq *lnd if svc.Config.FeeReserve { minimumBalance += svc.CalcFeeLimit(lnpayReq.PayReq.Destination, lnpayReq.PayReq.NumSatoshis) } - if currentBalance >= minimumBalance { + if currentBalance < minimumBalance { return &responses.NotEnoughBalanceError, nil } volume, err := svc.GetVolumeOverPeriod(ctx, userId, time.Duration(svc.Config.MaxVolumePeriod*int64(time.Second))) From 4a261e79d5b66ee2f5398767539b7eb125cacee7 Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Mon, 25 Sep 2023 10:52:38 +0200 Subject: [PATCH 05/19] fix responses --- controllers/keysend.ctrl.go | 2 +- controllers/payinvoice.ctrl.go | 2 +- controllers_v2/keysend.ctrl.go | 2 +- controllers_v2/payinvoice.ctrl.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/controllers/keysend.ctrl.go b/controllers/keysend.ctrl.go index 9ce3d52..add95a3 100644 --- a/controllers/keysend.ctrl.go +++ b/controllers/keysend.ctrl.go @@ -95,7 +95,7 @@ func (controller *KeySendController) KeySend(c echo.Context) error { } if resp != nil { c.Logger().Errorf("User does not have enough balance user_id:%v amount:%v", userID, lnPayReq.PayReq.NumSatoshis) - return c.JSON(http.StatusBadRequest, responses.NotEnoughBalanceError) + return c.JSON(http.StatusBadRequest, resp) } invoice, err := controller.svc.AddOutgoingInvoice(c.Request().Context(), userID, "", lnPayReq) if err != nil { diff --git a/controllers/payinvoice.ctrl.go b/controllers/payinvoice.ctrl.go index 2a67c72..91bbc69 100644 --- a/controllers/payinvoice.ctrl.go +++ b/controllers/payinvoice.ctrl.go @@ -110,7 +110,7 @@ func (controller *PayInvoiceController) PayInvoice(c echo.Context) error { } if resp != nil { c.Logger().Errorf("User does not have enough balance user_id:%v amount:%v", userID, lnPayReq.PayReq.NumSatoshis) - return c.JSON(http.StatusBadRequest, responses.NotEnoughBalanceError) + return c.JSON(http.StatusBadRequest, resp) } invoice, err := controller.svc.AddOutgoingInvoice(c.Request().Context(), userID, paymentRequest, lnPayReq) diff --git a/controllers_v2/keysend.ctrl.go b/controllers_v2/keysend.ctrl.go index fd8229d..68c4f8c 100644 --- a/controllers_v2/keysend.ctrl.go +++ b/controllers_v2/keysend.ctrl.go @@ -178,7 +178,7 @@ func (controller *KeySendController) SingleKeySend(c echo.Context, reqBody *KeyS } if resp != nil { c.Logger().Errorf("User does not have enough balance user_id:%v amount:%v", userID, lnPayReq.PayReq.NumSatoshis) - return nil, &responses.NotEnoughBalanceError + return nil, resp } invoice, err := controller.svc.AddOutgoingInvoice(c.Request().Context(), userID, "", lnPayReq) if err != nil { diff --git a/controllers_v2/payinvoice.ctrl.go b/controllers_v2/payinvoice.ctrl.go index 473eff5..ac14d64 100644 --- a/controllers_v2/payinvoice.ctrl.go +++ b/controllers_v2/payinvoice.ctrl.go @@ -111,7 +111,7 @@ func (controller *PayInvoiceController) PayInvoice(c echo.Context) error { } if resp != nil { c.Logger().Errorf("User does not have enough balance user_id:%v amount:%v", userID, lnPayReq.PayReq.NumSatoshis) - return c.JSON(http.StatusInternalServerError, responses.NotEnoughBalanceError) + return c.JSON(http.StatusInternalServerError, resp) } invoice, err := controller.svc.AddOutgoingInvoice(c.Request().Context(), userID, paymentRequest, lnPayReq) if err != nil { From 2a2614d77b6f9cebe355e7bdfac84688549b84cd Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Mon, 25 Sep 2023 13:55:18 +0200 Subject: [PATCH 06/19] fix existing tests --- integration_tests/util.go | 1 + lib/service/invoices_test.go | 3 +++ 2 files changed, 4 insertions(+) diff --git a/integration_tests/util.go b/integration_tests/util.go index 1d7812d..1c303bb 100644 --- a/integration_tests/util.go +++ b/integration_tests/util.go @@ -51,6 +51,7 @@ func LndHubTestServiceInit(lndClientMock lnd.LightningClientWrapper) (svc *servi DatabaseMaxConns: 1, DatabaseMaxIdleConns: 1, DatabaseConnMaxLifetime: 10, + MaxFeeAmount: 1000000, //todo: add max fee test JWTSecret: []byte("SECRET"), JWTAccessTokenExpiry: 3600, JWTRefreshTokenExpiry: 3600, diff --git a/lib/service/invoices_test.go b/lib/service/invoices_test.go index 6ae55d9..8da96da 100644 --- a/lib/service/invoices_test.go +++ b/lib/service/invoices_test.go @@ -10,6 +10,9 @@ import ( var svc = &LndhubService{ LndClient: &lnd.LNDWrapper{IdentityPubkey: "123pubkey"}, + Config: &Config{ + MaxFeeAmount: 1e6, + }, } func TestCalcFeeWithInvoiceLessThan1000(t *testing.T) { From b650b4e39682532500b802e3e764294cf59e61d6 Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Mon, 25 Sep 2023 14:01:38 +0200 Subject: [PATCH 07/19] add max fee test --- lib/service/invoices_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/service/invoices_test.go b/lib/service/invoices_test.go index 8da96da..5ee1536 100644 --- a/lib/service/invoices_test.go +++ b/lib/service/invoices_test.go @@ -45,3 +45,14 @@ func TestCalcFeeWithInvoiceMoreThan1000(t *testing.T) { expectedFee := int64(16) assert.Equal(t, expectedFee, feeLimit) } + +func TestCalcFeeWithMaxGlobalFee(t *testing.T) { + invoice := &models.Invoice{ + Amount: 1500, + } + svc.Config.MaxFeeAmount = 1 + + feeLimit := svc.CalcFeeLimit("dummy", invoice.Amount) + expectedFee := svc.Config.MaxFeeAmount + assert.Equal(t, expectedFee, feeLimit) +} From 0b9875340701f51e270b1912696cd44bca36ca72 Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Mon, 25 Sep 2023 14:39:38 +0200 Subject: [PATCH 08/19] add integration test max volume --- integration_tests/internal_payment_test.go | 61 ++++++++++++++++++++++ integration_tests/util.go | 2 +- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/integration_tests/internal_payment_test.go b/integration_tests/internal_payment_test.go index 3d117fa..ec68810 100644 --- a/integration_tests/internal_payment_test.go +++ b/integration_tests/internal_payment_test.go @@ -133,6 +133,67 @@ func (suite *PaymentTestSuite) TestPaymentFeeReserve() { //reset fee reserve so it's not used in other tests suite.service.Config.FeeReserve = false } +func (suite *PaymentTestSuite) TestVolumeExceeded() { + //this will cause the payment to fail as the account was already funded + //with 1000 sats + suite.service.Config.MaxVolume = 999 + suite.service.Config.MaxVolumePeriod = 2592000 + aliceFundingSats := 1000 + //fund alice account + invoiceResponse := suite.createAddInvoiceReq(aliceFundingSats, "integration test internal payment alice", suite.aliceToken) + err := suite.mlnd.mockPaidInvoice(invoiceResponse, 0, false, nil) + assert.NoError(suite.T(), err) + + //wait a bit for the payment to be processed + time.Sleep(10 * time.Millisecond) + + //try to make external payment + //which should fail + //create external invoice + externalSatRequested := 1000 + externalInvoice := lnrpc.Invoice{ + Memo: "integration tests: external pay from user", + Value: int64(externalSatRequested), + } + invoice, err := suite.externalLND.AddInvoice(context.Background(), &externalInvoice) + assert.NoError(suite.T(), err) + //pay external invoice + rec := httptest.NewRecorder() + var buf bytes.Buffer + assert.NoError(suite.T(), json.NewEncoder(&buf).Encode(&ExpectedPayInvoiceRequestBody{ + Invoice: invoice.PaymentRequest, + })) + req := httptest.NewRequest(http.MethodPost, "/payinvoice", &buf) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", suite.aliceToken)) + suite.echo.ServeHTTP(rec, req) + //should fail because max volume check + assert.Equal(suite.T(), http.StatusBadRequest, rec.Code) + resp := &responses.ErrorResponse{} + err = json.NewDecoder(rec.Body).Decode(resp) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), responses.TooMuchVolumeError.Message, resp.Message) + + //change the period to be 1 second, sleep for 2 seconds, try to make another payment, this should work + suite.service.Config.MaxVolumePeriod = 1 + time.Sleep(2 * time.Second) + rec = httptest.NewRecorder() + externalInvoice = lnrpc.Invoice{ + Memo: "integration tests: external pay from user", + Value: int64(externalSatRequested), + } + invoice, err = suite.externalLND.AddInvoice(context.Background(), &externalInvoice) + assert.NoError(suite.T(), err) + assert.NoError(suite.T(), json.NewEncoder(&buf).Encode(&ExpectedPayInvoiceRequestBody{ + Invoice: invoice.PaymentRequest, + })) + suite.echo.ServeHTTP(rec, req) + assert.Equal(suite.T(), http.StatusOK, rec.Code) + + //change the config back + suite.service.Config.MaxVolumePeriod = 0 + suite.service.Config.MaxVolume = 1e6 +} func (suite *PaymentTestSuite) TestInternalPayment() { aliceFundingSats := 1000 bobSatRequested := 500 diff --git a/integration_tests/util.go b/integration_tests/util.go index 1c303bb..959feeb 100644 --- a/integration_tests/util.go +++ b/integration_tests/util.go @@ -51,7 +51,7 @@ func LndHubTestServiceInit(lndClientMock lnd.LightningClientWrapper) (svc *servi DatabaseMaxConns: 1, DatabaseMaxIdleConns: 1, DatabaseConnMaxLifetime: 10, - MaxFeeAmount: 1000000, //todo: add max fee test + MaxFeeAmount: 1e6, //todo: add max fee test JWTSecret: []byte("SECRET"), JWTAccessTokenExpiry: 3600, JWTRefreshTokenExpiry: 3600, From 881c7b45bb40b69c03c845992c7c3f2c4754a9fb Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Mon, 25 Sep 2023 14:50:33 +0200 Subject: [PATCH 09/19] remove todo --- integration_tests/util.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_tests/util.go b/integration_tests/util.go index 959feeb..2fe3e7d 100644 --- a/integration_tests/util.go +++ b/integration_tests/util.go @@ -51,7 +51,7 @@ func LndHubTestServiceInit(lndClientMock lnd.LightningClientWrapper) (svc *servi DatabaseMaxConns: 1, DatabaseMaxIdleConns: 1, DatabaseConnMaxLifetime: 10, - MaxFeeAmount: 1e6, //todo: add max fee test + MaxFeeAmount: 1e6, JWTSecret: []byte("SECRET"), JWTAccessTokenExpiry: 3600, JWTRefreshTokenExpiry: 3600, From 4f11e62d2b9d1213a9c83765ed7edd7e457c4f9b Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Mon, 25 Sep 2023 14:53:39 +0200 Subject: [PATCH 10/19] add sentry capture --- lib/service/user.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/service/user.go b/lib/service/user.go index 0e27d77..553de40 100644 --- a/lib/service/user.go +++ b/lib/service/user.go @@ -12,6 +12,7 @@ import ( "github.com/getAlby/lndhub.go/lib/responses" "github.com/getAlby/lndhub.go/lib/security" "github.com/getAlby/lndhub.go/lnd" + "github.com/getsentry/sentry-go" "github.com/uptrace/bun" passwordvalidator "github.com/wagslane/go-password-validator" ) @@ -141,6 +142,7 @@ func (svc *LndhubService) CheckPaymentAllowed(ctx context.Context, lnpayReq *lnd return nil, err } if volume > svc.Config.MaxVolume { + sentry.CaptureMessage(fmt.Sprintf("transaction volume exceeded for user %d", userId)) return &responses.TooMuchVolumeError, nil } return nil, nil From 846f3c09d59bad5daf5689c89f758d7acbb89e3a Mon Sep 17 00:00:00 2001 From: im-adithya Date: Mon, 25 Sep 2023 18:40:11 +0530 Subject: [PATCH 11/19] chore: publish invoice data before executing payment --- controllers/keysend.ctrl.go | 8 ++++---- lib/service/invoices.go | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/controllers/keysend.ctrl.go b/controllers/keysend.ctrl.go index 586111c..8d80371 100644 --- a/controllers/keysend.ctrl.go +++ b/controllers/keysend.ctrl.go @@ -93,6 +93,10 @@ func (controller *KeySendController) KeySend(c echo.Context) error { if err != nil { return err } + if _, err := hex.DecodeString(invoice.DestinationPubkeyHex); err != nil || len(invoice.DestinationPubkeyHex) != common.DestinationPubkeyHexSize { + c.Logger().Errorf("Invalid destination pubkey hex user_id:%v pubkey:%v", userID, len(invoice.DestinationPubkeyHex)) + return c.JSON(http.StatusBadRequest, responses.InvalidDestinationError) + } invoice.DestinationCustomRecords = map[uint64][]byte{} for key, value := range reqBody.CustomRecords { intKey, err := strconv.Atoi(key) @@ -101,10 +105,6 @@ func (controller *KeySendController) KeySend(c echo.Context) error { } invoice.DestinationCustomRecords[uint64(intKey)] = []byte(value) } - if _, err := hex.DecodeString(invoice.DestinationPubkeyHex); err != nil || len(invoice.DestinationPubkeyHex) != common.DestinationPubkeyHexSize { - c.Logger().Errorf("Invalid destination pubkey hex user_id:%v pubkey:%v", userID, len(invoice.DestinationPubkeyHex)) - return c.JSON(http.StatusBadRequest, responses.InvalidDestinationError) - } sendPaymentResponse, err := controller.svc.PayInvoice(c.Request().Context(), invoice) if err != nil { c.Logger().Errorf("Payment failed: user_id:%v error: %v", userID, err) diff --git a/lib/service/invoices.go b/lib/service/invoices.go index 634cee2..9473e08 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -123,6 +123,9 @@ func (svc *LndhubService) SendPaymentSync(ctx context.Context, invoice *models.I return sendPaymentResponse, err } + // Save invoice details before executing payment + svc.InvoicePubSub.Publish(common.InvoiceTypeOutgoing, *invoice) + // Execute the payment sendPaymentResult, err := svc.LndClient.SendPaymentSync(ctx, sendPaymentRequest) if err != nil { @@ -172,6 +175,7 @@ func (svc *LndhubService) createLnRpcSendRequest(invoice *models.Invoice) (*lnrp if err != nil { return nil, err } + invoice.RHash = hex.EncodeToString(pHash.Sum(nil)) invoice.DestinationCustomRecords[KEYSEND_CUSTOM_RECORD] = preImage return &lnrpc.SendRequest{ Dest: destBytes, From da3adf75d2eada4d80bce24bcbad4c3f4ae3b887 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Mon, 25 Sep 2023 19:56:46 +0530 Subject: [PATCH 12/19] fix: add r_hash and preimage to keysend invoices --- lib/service/invoices.go | 32 ++++++++++++++++++++---------- lib/service/invoicesubscription.go | 11 ++-------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/lib/service/invoices.go b/lib/service/invoices.go index 9473e08..3a8cd6b 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -123,9 +123,6 @@ func (svc *LndhubService) SendPaymentSync(ctx context.Context, invoice *models.I return sendPaymentResponse, err } - // Save invoice details before executing payment - svc.InvoicePubSub.Publish(common.InvoiceTypeOutgoing, *invoice) - // Execute the payment sendPaymentResult, err := svc.LndClient.SendPaymentSync(ctx, sendPaymentRequest) if err != nil { @@ -163,24 +160,25 @@ func (svc *LndhubService) createLnRpcSendRequest(invoice *models.Invoice) (*lnrp }, nil } - preImage, err := makePreimageHex() - if err != nil { - return nil, err - } - pHash := sha256.New() - pHash.Write(preImage) // Prepare the LNRPC call //See: https://github.com/hsjoberg/blixt-wallet/blob/9fcc56a7dc25237bc14b85e6490adb9e044c009c/src/lndmobile/index.ts#L251-L270 destBytes, err := hex.DecodeString(invoice.DestinationPubkeyHex) if err != nil { return nil, err } - invoice.RHash = hex.EncodeToString(pHash.Sum(nil)) + preImage, err := hex.DecodeString(invoice.Preimage) + if err != nil { + return nil, err + } invoice.DestinationCustomRecords[KEYSEND_CUSTOM_RECORD] = preImage + paymentHash, err := hex.DecodeString(invoice.RHash) + if err != nil { + return nil, err + } return &lnrpc.SendRequest{ Dest: destBytes, Amt: invoice.Amount, - PaymentHash: pHash.Sum(nil), + PaymentHash: paymentHash, FeeLimit: &feeLimit, DestFeatures: []lnrpc.FeatureBit{lnrpc.FeatureBit_TLV_ONION_REQ}, DestCustomRecords: invoice.DestinationCustomRecords, @@ -457,6 +455,18 @@ func (svc *LndhubService) AddOutgoingInvoice(ctx context.Context, userID int64, ExpiresAt: bun.NullTime{Time: time.Unix(lnPayReq.PayReq.Timestamp, 0).Add(time.Duration(lnPayReq.PayReq.Expiry) * time.Second)}, } + if lnPayReq.Keysend { + preImage, err := makePreimageHex() + if err != nil { + return nil, err + } + pHash := sha256.New() + pHash.Write(preImage) + + invoice.RHash = hex.EncodeToString(pHash.Sum(nil)) + invoice.Preimage = hex.EncodeToString(preImage) + } + // Save invoice _, err := svc.DB.NewInsert().Model(&invoice).Exec(ctx) if err != nil { diff --git a/lib/service/invoicesubscription.go b/lib/service/invoicesubscription.go index b9d24f8..8e1083d 100644 --- a/lib/service/invoicesubscription.go +++ b/lib/service/invoicesubscription.go @@ -2,7 +2,6 @@ package service import ( "context" - "crypto/sha256" "database/sql" "encoding/hex" "errors" @@ -27,12 +26,6 @@ func (svc *LndhubService) HandleInternalKeysendPayment(ctx context.Context, invo if err != nil { return nil, err } - preImage, err := makePreimageHex() - if err != nil { - return nil, err - } - pHash := sha256.New() - pHash.Write(preImage) expiry := time.Hour * 24 incomingInvoice := models.Invoice{ Type: common.InvoiceTypeIncoming, @@ -43,8 +36,8 @@ func (svc *LndhubService) HandleInternalKeysendPayment(ctx context.Context, invo State: common.InvoiceStateInitialized, ExpiresAt: bun.NullTime{Time: time.Now().Add(expiry)}, Keysend: true, - RHash: hex.EncodeToString(pHash.Sum(nil)), - Preimage: hex.EncodeToString(preImage), + RHash: invoice.RHash, + Preimage: invoice.Preimage, DestinationCustomRecords: invoice.DestinationCustomRecords, DestinationPubkeyHex: svc.LndClient.GetMainPubkey(), AddIndex: invoice.AddIndex, From 43956fdf7d4ef5cc0edcff99167580c2cbfff322 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Wed, 27 Sep 2023 15:46:12 +0530 Subject: [PATCH 13/19] chore: add test for failing keysend --- integration_tests/keysend_failure_test.go | 118 ++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 integration_tests/keysend_failure_test.go diff --git a/integration_tests/keysend_failure_test.go b/integration_tests/keysend_failure_test.go new file mode 100644 index 0000000..25659c7 --- /dev/null +++ b/integration_tests/keysend_failure_test.go @@ -0,0 +1,118 @@ +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/go-playground/validator/v10" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type KeySendFailureTestSuite struct { + TestSuite + service *service.LndhubService + mlnd *MockLND + aliceLogin ExpectedCreateUserResponseBody + aliceToken string + invoiceUpdateSubCancelFn context.CancelFunc + serviceClient *LNDMockWrapperAsync +} + +func (suite *KeySendFailureTestSuite) TearDownTest() { + clearTable(suite.service, "transaction_entries") + clearTable(suite.service, "invoices") +} + +func (suite *KeySendFailureTestSuite) TearDownSuite() { + suite.invoiceUpdateSubCancelFn() +} + +func (suite *KeySendFailureTestSuite) SetupSuite() { + fee := int64(1) + mlnd := newDefaultMockLND() + mlnd.fee = fee + suite.mlnd = mlnd + // inject fake lnd client with failing send payment sync into service + lndClient, err := NewLNDMockWrapperAsync(mlnd) + suite.serviceClient = lndClient + if err != nil { + log.Fatalf("Error setting up test client: %v", err) + } + svc, err := LndHubTestServiceInit(lndClient) + if err != nil { + log.Fatalf("Error initializing test service: %v", err) + } + users, userTokens, err := createUsers(svc, 1) + if err != nil { + log.Fatalf("Error creating test users: %v", err) + } + // Subscribe to LND invoice updates in the background + // store cancel func to be called in tear down suite + ctx, cancel := context.WithCancel(context.Background()) + suite.invoiceUpdateSubCancelFn = cancel + go svc.InvoiceUpdateSubscription(ctx) + suite.service = svc + e := echo.New() + + e.HTTPErrorHandler = responses.HTTPErrorHandler + e.Validator = &lib.CustomValidator{Validator: validator.New()} + suite.echo = e + assert.Equal(suite.T(), 1, len(users)) + assert.Equal(suite.T(), 1, len(userTokens)) + suite.aliceLogin = users[0] + suite.aliceToken = userTokens[0] + suite.echo.Use(tokens.Middleware([]byte(suite.service.Config.JWTSecret))) + suite.echo.POST("/addinvoice", controllers.NewAddInvoiceController(suite.service).AddInvoice) + suite.echo.POST("/keysend", controllers.NewKeySendController(suite.service).KeySend) +} + + +func (suite *KeySendFailureTestSuite) TestKeysendPayment() { + aliceFundingSats := 1000 + externalSatRequested := 500 + //fund alice account + invoiceResponse := suite.createAddInvoiceReq(aliceFundingSats, "integration test external payment alice", suite.aliceToken) + err := suite.mlnd.mockPaidInvoice(invoiceResponse, 0, false, nil) + assert.NoError(suite.T(), err) + + //wait a bit for the callback event to hit + time.Sleep(10 * time.Millisecond) + + go suite.createKeySendReqError(int64(externalSatRequested), "key send test", "123456789012345678901234567890123456789012345678901234567890abcdef", suite.aliceToken) + + suite.serviceClient.FailPayment(SendPaymentMockError) + time.Sleep(2 * time.Second) + + // check that balance was reverted + 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(aliceFundingSats), aliceBalance) + + 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) + assert.NotEmpty(suite.T(), invoices[0].RHash) + assert.NotEmpty(suite.T(), invoices[0].Preimage) +} + +func TestKeySendFailureTestSuite(t *testing.T) { + suite.Run(t, new(KeySendFailureTestSuite)) +} From 8505f05b38b3d12545e1315d9abaab4776d506b7 Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Wed, 27 Sep 2023 13:44:07 +0200 Subject: [PATCH 14/19] disable volume check by default --- lib/service/config.go | 2 +- lib/service/user.go | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/service/config.go b/lib/service/config.go index ad9efd5..fa53971 100644 --- a/lib/service/config.go +++ b/lib/service/config.go @@ -37,7 +37,7 @@ type Config struct { MaxSendAmount int64 `envconfig:"MAX_SEND_AMOUNT" default:"0"` MaxAccountBalance int64 `envconfig:"MAX_ACCOUNT_BALANCE" default:"0"` MaxFeeAmount int64 `envconfig:"MAX_FEE_AMOUNT" default:"5000"` - MaxVolume int64 `envconfig:"MAX_VOLUME" default:"5000000"` + MaxVolume int64 `envconfig:"MAX_VOLUME" default:"0"` //0 means the volume check is disabled by default MaxVolumePeriod int64 `envconfig:"MAX_VOLUME_PERIOD" default:"2592000"` //in seconds, default 1 month RabbitMQUri string `envconfig:"RABBITMQ_URI"` RabbitMQLndhubInvoiceExchange string `envconfig:"RABBITMQ_INVOICE_EXCHANGE" default:"lndhub_invoice"` diff --git a/lib/service/user.go b/lib/service/user.go index 553de40..80b6354 100644 --- a/lib/service/user.go +++ b/lib/service/user.go @@ -137,13 +137,16 @@ func (svc *LndhubService) CheckPaymentAllowed(ctx context.Context, lnpayReq *lnd if currentBalance < minimumBalance { return &responses.NotEnoughBalanceError, nil } - volume, err := svc.GetVolumeOverPeriod(ctx, userId, time.Duration(svc.Config.MaxVolumePeriod*int64(time.Second))) - if err != nil { - return nil, err - } - if volume > svc.Config.MaxVolume { - sentry.CaptureMessage(fmt.Sprintf("transaction volume exceeded for user %d", userId)) - return &responses.TooMuchVolumeError, nil + //only check for volume if configured + if svc.Config.MaxVolume > 0 { + volume, err := svc.GetVolumeOverPeriod(ctx, userId, time.Duration(svc.Config.MaxVolumePeriod*int64(time.Second))) + if err != nil { + return nil, err + } + if volume > svc.Config.MaxVolume { + sentry.CaptureMessage(fmt.Sprintf("transaction volume exceeded for user %d", userId)) + return &responses.TooMuchVolumeError, nil + } } return nil, nil } From 943c257c657fff15e74f87cd8627cb2a3adbe94b Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Thu, 28 Sep 2023 14:46:51 +0200 Subject: [PATCH 15/19] tests: remove unnecessary config value --- integration_tests/util.go | 1 - 1 file changed, 1 deletion(-) diff --git a/integration_tests/util.go b/integration_tests/util.go index 2fe3e7d..1d7812d 100644 --- a/integration_tests/util.go +++ b/integration_tests/util.go @@ -51,7 +51,6 @@ func LndHubTestServiceInit(lndClientMock lnd.LightningClientWrapper) (svc *servi DatabaseMaxConns: 1, DatabaseMaxIdleConns: 1, DatabaseConnMaxLifetime: 10, - MaxFeeAmount: 1e6, JWTSecret: []byte("SECRET"), JWTAccessTokenExpiry: 3600, JWTRefreshTokenExpiry: 3600, From ab14adf96577e5bffe927bef82e2043cd8b7c681 Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Thu, 28 Sep 2023 14:58:31 +0200 Subject: [PATCH 16/19] remove redundant settle state in query --- lib/service/user.go | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/service/user.go b/lib/service/user.go index 80b6354..26d6d4d 100644 --- a/lib/service/user.go +++ b/lib/service/user.go @@ -208,7 +208,6 @@ func (svc *LndhubService) GetVolumeOverPeriod(ctx context.Context, userId int64, err = svc.DB.NewSelect().Table("invoices"). ColumnExpr("sum(invoices.amount) as result"). Where("invoices.user_id = ?", userId). - Where("invoices.state = ?", common.InvoiceStateSettled). Where("invoices.settled_at >= ?", time.Now().Add(-1*period)). Scan(ctx, &result) if err != nil { From 4b019a4692367be00781725fbbfe27841bd71b0b Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Thu, 28 Sep 2023 17:05:11 +0400 Subject: [PATCH 17/19] Add index for invoice sums --- db/migrations/20230928130000_add_invoice_settled_index.up.sql | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 db/migrations/20230928130000_add_invoice_settled_index.up.sql diff --git a/db/migrations/20230928130000_add_invoice_settled_index.up.sql b/db/migrations/20230928130000_add_invoice_settled_index.up.sql new file mode 100644 index 0000000..e4b547b --- /dev/null +++ b/db/migrations/20230928130000_add_invoice_settled_index.up.sql @@ -0,0 +1,3 @@ +CREATE INDEX CONCURRENTLY IF NOT EXISTS index_invoices_on_user_id_settled_at + ON invoices(user_id, settled_at) + INCLUDE(amount); From 7d62fd0ab7a004da0b35c83841ab0c945f3477a8 Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Thu, 28 Sep 2023 16:42:27 +0200 Subject: [PATCH 18/19] turns out this was necessary for tests --- integration_tests/util.go | 1 + 1 file changed, 1 insertion(+) diff --git a/integration_tests/util.go b/integration_tests/util.go index 1d7812d..2fe3e7d 100644 --- a/integration_tests/util.go +++ b/integration_tests/util.go @@ -51,6 +51,7 @@ func LndHubTestServiceInit(lndClientMock lnd.LightningClientWrapper) (svc *servi DatabaseMaxConns: 1, DatabaseMaxIdleConns: 1, DatabaseConnMaxLifetime: 10, + MaxFeeAmount: 1e6, JWTSecret: []byte("SECRET"), JWTAccessTokenExpiry: 3600, JWTRefreshTokenExpiry: 3600, From feaabb98407ed3f96f64c97dd96613fc1e26494f Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Thu, 28 Sep 2023 16:47:26 +0200 Subject: [PATCH 19/19] remove codecov it is annoying me --- .github/workflows/integration_tests.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/integration_tests.yaml b/.github/workflows/integration_tests.yaml index ad3235d..3acf1ef 100644 --- a/.github/workflows/integration_tests.yaml +++ b/.github/workflows/integration_tests.yaml @@ -43,6 +43,4 @@ jobs: - name: Run tests run: go test -p 1 -v -covermode=atomic -coverprofile=coverage.out -cover -coverpkg=./... ./... env: - RABBITMQ_URI: amqp://root:password@localhost:5672 - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v3 + RABBITMQ_URI: amqp://root:password@localhost:5672 \ No newline at end of file