From badb700837ea463cc955a72c3785f98b4b2f2c91 Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Fri, 5 Apr 2024 12:51:08 +0530 Subject: [PATCH] chore: allow 0 limits for send/receive amount/volume (#486) * chore: allow 0 limits for send/receive amount/volume * fix: use pointers for claims * fix: tests to set default as -1 * chore: add 0 check in outgoing exceeded tests * chore: also default max account balance to -1 * chore: remove dbUri :P * chore: remove print statements * chore: add more tests for limits --- integration_tests/internal_payment_test.go | 52 +++++++++++++++++++--- integration_tests/invoice_test.go | 20 +++++++++ integration_tests/keysend_test.go | 43 +++++++++++++++++- integration_tests/outgoing_payment_test.go | 26 ++++++++++- integration_tests/util.go | 5 +++ lib/service/config.go | 10 ++--- lib/service/user.go | 32 ++++++------- lib/tokens/jwt.go | 14 +++--- 8 files changed, 165 insertions(+), 37 deletions(-) diff --git a/integration_tests/internal_payment_test.go b/integration_tests/internal_payment_test.go index 6590097..4b2c721 100644 --- a/integration_tests/internal_payment_test.go +++ b/integration_tests/internal_payment_test.go @@ -163,12 +163,30 @@ func (suite *PaymentTestSuite) TestIncomingExceededChecks() { assert.Equal(suite.T(), responses.ReceiveExceededError.Message, resp.Message) // remove volume and receive config and check if it works - suite.service.Config.MaxReceiveAmount = 0 + suite.service.Config.MaxReceiveAmount = -1 invoiceResponse = suite.createAddInvoiceReq(aliceFundingSats, "integration test internal payment alice", suite.aliceToken) err = suite.mlnd.mockPaidInvoice(invoiceResponse, 0, false, nil) assert.NoError(suite.T(), err) - // add max account + // check if setting zero as receive amount stops + suite.service.Config.MaxReceiveAmount = 0 + assert.NoError(suite.T(), json.NewEncoder(&buf).Encode(&ExpectedAddInvoiceRequestBody{ + Amount: aliceFundingSats, + Memo: "memo", + })) + req = httptest.NewRequest(http.MethodPost, "/addinvoice", &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 receive amount 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.ReceiveExceededError.Message, resp.Message) + + // remove max receive and add max account + suite.service.Config.MaxReceiveAmount = -1 suite.service.Config.MaxAccountBalance = 500 assert.NoError(suite.T(), json.NewEncoder(&buf).Encode(&ExpectedAddInvoiceRequestBody{ Amount: aliceFundingSats, @@ -186,7 +204,7 @@ func (suite *PaymentTestSuite) TestIncomingExceededChecks() { assert.Equal(suite.T(), responses.BalanceExceededError.Message, resp.Message) //change the config back and add sats, it should work now - suite.service.Config.MaxAccountBalance = 0 + suite.service.Config.MaxAccountBalance = -1 invoiceResponse = suite.createAddInvoiceReq(aliceFundingSats, "integration test internal payment alice", suite.aliceToken) err = suite.mlnd.mockPaidInvoice(invoiceResponse, 0, false, nil) assert.NoError(suite.T(), err) @@ -210,7 +228,7 @@ func (suite *PaymentTestSuite) TestIncomingExceededChecks() { assert.Equal(suite.T(), responses.TooMuchVolumeError.Message, resp.Message) //change the config back, it should work now - suite.service.Config.MaxReceiveVolume = 0 + suite.service.Config.MaxReceiveVolume = -1 suite.service.Config.MaxVolumePeriod = 0 invoiceResponse = suite.createAddInvoiceReq(aliceFundingSats, "integration test internal payment alice", suite.aliceToken) err = suite.mlnd.mockPaidInvoice(invoiceResponse, 0, false, nil) @@ -291,9 +309,31 @@ func (suite *PaymentTestSuite) TestOutgoingExceededChecks() { assert.NoError(suite.T(), err) assert.Equal(suite.T(), responses.TooMuchVolumeError.Message, resp.Message) - //change the config back - suite.service.Config.MaxSendAmount = 0 + // check if setting zero as send volume stops suite.service.Config.MaxSendVolume = 0 + //volume + invoice, err = suite.externalLND.AddInvoice(context.Background(), &externalInvoice) + assert.NoError(suite.T(), err) + //pay external invoice + rec = httptest.NewRecorder() + 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 0 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 config back + suite.service.Config.MaxSendAmount = -1 + suite.service.Config.MaxSendVolume = -1 suite.service.Config.MaxVolumePeriod = 0 } diff --git a/integration_tests/invoice_test.go b/integration_tests/invoice_test.go index 083671c..a0d2392 100644 --- a/integration_tests/invoice_test.go +++ b/integration_tests/invoice_test.go @@ -84,6 +84,26 @@ func (suite *InvoiceTestSuite) TestAddInvoiceWithoutToken() { assert.Equal(suite.T(), 1, len(invoicesAfter)) } +func (suite *InvoiceTestSuite) TestAddInvoiceWithLimits() { + suite.service.Config.MaxReceiveAmount = 200 + rec := httptest.NewRecorder() + var buf bytes.Buffer + assert.NoError(suite.T(), json.NewEncoder(&buf).Encode(&ExpectedV2AddInvoiceRequestBody{ + Amount: 300, + Memo: "testing limits", + })) + req := httptest.NewRequest(http.MethodPost, "/v2/invoices", &buf) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", suite.aliceToken)) + suite.echo.ServeHTTP(rec, req) + //should fail because max receive amount 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.ReceiveExceededError.Message, resp.Message) +} + func (suite *InvoiceTestSuite) TestAddInvoiceForNonExistingUser() { nonExistingLogin := suite.aliceLogin.Login + "abc" suite.createInvoiceReqError(10, "test invoice without token", nonExistingLogin) diff --git a/integration_tests/keysend_test.go b/integration_tests/keysend_test.go index 58da0f8..102b4c6 100644 --- a/integration_tests/keysend_test.go +++ b/integration_tests/keysend_test.go @@ -79,7 +79,7 @@ func (suite *KeySendTestSuite) TearDownSuite() { func (suite *KeySendTestSuite) TestKeysendPayment() { suite.service.Config.ServiceFee = 1 - aliceFundingSats := 1000 + aliceFundingSats := 1500 externalSatRequested := 500 expectedServiceFee := 1 //fund alice account @@ -99,6 +99,47 @@ func (suite *KeySendTestSuite) TestKeysendPayment() { } assert.Equal(suite.T(), int64(aliceFundingSats)-int64(externalSatRequested+int(suite.mlnd.fee)+expectedServiceFee), aliceBalance) suite.service.Config.ServiceFee = 0 + + var buf bytes.Buffer + suite.service.Config.MaxSendAmount = 200 + rec := httptest.NewRecorder() + assert.NoError(suite.T(), json.NewEncoder(&buf).Encode(&ExpectedKeySendRequestBody{ + Amount: int64(externalSatRequested), + Destination: "123456789012345678901234567890123456789012345678901234567890abcdef", + Memo: "key send test", + })) + req := httptest.NewRequest(http.MethodPost, "/keysend", &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 send amount 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.SendExceededError.Message, resp.Message) + + // check if setting zero as send amount stops + suite.service.Config.MaxSendAmount = 0 + assert.NoError(suite.T(), json.NewEncoder(&buf).Encode(&ExpectedKeySendRequestBody{ + Amount: int64(externalSatRequested), + Destination: "123456789012345678901234567890123456789012345678901234567890abcdef", + Memo: "key send test", + })) + req = httptest.NewRequest(http.MethodPost, "/keysend", &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 send amount 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.SendExceededError.Message, resp.Message) + + // restore default and try again, should work now + suite.service.Config.MaxSendAmount = -1 + suite.createKeySendReq(int64(externalSatRequested), "key send test", "123456789012345678901234567890123456789012345678901234567890abcdef", suite.aliceToken) } func (suite *KeySendTestSuite) TestKeysendPaymentNonExistentDestination() { diff --git a/integration_tests/outgoing_payment_test.go b/integration_tests/outgoing_payment_test.go index efa7d8b..891bd73 100644 --- a/integration_tests/outgoing_payment_test.go +++ b/integration_tests/outgoing_payment_test.go @@ -1,6 +1,7 @@ package integration_tests import ( + "bytes" "context" "encoding/json" "fmt" @@ -9,6 +10,8 @@ import ( "time" "github.com/getAlby/lndhub.go/common" + "github.com/getAlby/lndhub.go/lib/responses" + "github.com/labstack/echo/v4" "github.com/lightningnetwork/lnd/lnrpc" "github.com/stretchr/testify/assert" ) @@ -35,6 +38,25 @@ func (suite *PaymentTestSuite) TestOutGoingPayment() { } invoice, err := suite.externalLND.AddInvoice(context.Background(), &externalInvoice) assert.NoError(suite.T(), err) + + var buf bytes.Buffer + suite.service.Config.MaxSendAmount = 200 + rec := httptest.NewRecorder() + 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 send amount 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.SendExceededError.Message, resp.Message) + + suite.service.Config.MaxSendAmount = -1 //pay external from alice payResponse := suite.createPayInvoiceReq(&ExpectedPayInvoiceRequestBody{ Invoice: invoice.PaymentRequest, @@ -116,9 +138,9 @@ func (suite *PaymentTestSuite) TestOutGoingPayment() { // fetch transactions, make sure the fee is there // check invoices again - req := httptest.NewRequest(http.MethodGet, "/gettxs", nil) + req = httptest.NewRequest(http.MethodGet, "/gettxs", nil) req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", suite.aliceToken)) - rec := httptest.NewRecorder() + rec = httptest.NewRecorder() suite.echo.ServeHTTP(rec, req) responseBody := &[]ExpectedOutgoingInvoice{} assert.Equal(suite.T(), http.StatusOK, rec.Code) diff --git a/integration_tests/util.go b/integration_tests/util.go index 4d4fb48..3a9b6e9 100644 --- a/integration_tests/util.go +++ b/integration_tests/util.go @@ -60,6 +60,11 @@ func LndHubTestServiceInit(lndClientMock lnd.LightningClientWrapper) (svc *servi JWTSecret: []byte("SECRET"), JWTAccessTokenExpiry: 3600, JWTRefreshTokenExpiry: 3600, + MaxSendAmount: -1, + MaxSendVolume: -1, + MaxReceiveAmount: -1, + MaxReceiveVolume: -1, + MaxAccountBalance: -1, } rabbitmqUri, ok := os.LookupEnv("RABBITMQ_URI") diff --git a/lib/service/config.go b/lib/service/config.go index de83de4..7e0cf5a 100644 --- a/lib/service/config.go +++ b/lib/service/config.go @@ -35,12 +35,12 @@ type Config struct { NoServiceFeeUpToAmount int `envconfig:"NO_SERVICE_FEE_UP_TO_AMOUNT" default:"0"` AllowAccountCreation bool `envconfig:"ALLOW_ACCOUNT_CREATION" default:"true"` MinPasswordEntropy int `envconfig:"MIN_PASSWORD_ENTROPY" default:"0"` - MaxReceiveAmount int64 `envconfig:"MAX_RECEIVE_AMOUNT" default:"0"` - MaxSendAmount int64 `envconfig:"MAX_SEND_AMOUNT" default:"0"` - MaxAccountBalance int64 `envconfig:"MAX_ACCOUNT_BALANCE" default:"0"` + MaxReceiveAmount int64 `envconfig:"MAX_RECEIVE_AMOUNT" default:"-1"` + MaxSendAmount int64 `envconfig:"MAX_SEND_AMOUNT" default:"-1"` + MaxAccountBalance int64 `envconfig:"MAX_ACCOUNT_BALANCE" default:"-1"` MaxFeeAmount int64 `envconfig:"MAX_FEE_AMOUNT" default:"5000"` - MaxSendVolume int64 `envconfig:"MAX_SEND_VOLUME" default:"0"` //0 means the volume check is disabled by default - MaxReceiveVolume int64 `envconfig:"MAX_RECEIVE_VOLUME" default:"0"` //0 means the volume check is disabled by default + MaxSendVolume int64 `envconfig:"MAX_SEND_VOLUME" default:"-1"` //-1 means the volume check is disabled by default + MaxReceiveVolume int64 `envconfig:"MAX_RECEIVE_VOLUME" default:"-1"` //-1 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 c20af31..6b21bd8 100644 --- a/lib/service/user.go +++ b/lib/service/user.go @@ -102,7 +102,7 @@ func (svc *LndhubService) UpdateUser(ctx context.Context, userId int64, login *s // if a user gets deleted we mark it as deactivated and deleted // un-deleting it is not supported currently if deleted != nil { - if *deleted == true { + if *deleted { user.Deactivated = true user.Deleted = true } @@ -136,14 +136,14 @@ func (svc *LndhubService) FindUserByLogin(ctx context.Context, login string) (*m func (svc *LndhubService) CheckOutgoingPaymentAllowed(c echo.Context, lnpayReq *lnd.LNPayReq, userId int64) (result *responses.ErrorResponse, err error) { limits := svc.GetLimits(c) - if limits.MaxSendAmount > 0 { + if limits.MaxSendAmount >= 0 { if lnpayReq.PayReq.NumSatoshis > limits.MaxSendAmount { svc.Logger.Errorf("Max send amount exceeded for user_id %v (amount:%v)", userId, lnpayReq.PayReq.NumSatoshis) return &responses.SendExceededError, nil } } - if limits.MaxSendVolume > 0 { + if limits.MaxSendVolume >= 0 { volume, err := svc.GetVolumeOverPeriod(c.Request().Context(), userId, common.InvoiceTypeOutgoing, time.Duration(svc.Config.MaxVolumePeriod*int64(time.Second))) if err != nil { svc.Logger.Errorj( @@ -195,14 +195,14 @@ func (svc *LndhubService) CheckOutgoingPaymentAllowed(c echo.Context, lnpayReq * func (svc *LndhubService) CheckIncomingPaymentAllowed(c echo.Context, amount, userId int64) (result *responses.ErrorResponse, err error) { limits := svc.GetLimits(c) - if limits.MaxReceiveAmount > 0 { + if limits.MaxReceiveAmount >= 0 { if amount > limits.MaxReceiveAmount { svc.Logger.Errorf("Max receive amount exceeded for user_id %d", userId) return &responses.ReceiveExceededError, nil } } - if limits.MaxReceiveVolume > 0 { + if limits.MaxReceiveVolume >= 0 { volume, err := svc.GetVolumeOverPeriod(c.Request().Context(), userId, common.InvoiceTypeIncoming, time.Duration(svc.Config.MaxVolumePeriod*int64(time.Second))) if err != nil { svc.Logger.Errorj( @@ -221,7 +221,7 @@ func (svc *LndhubService) CheckIncomingPaymentAllowed(c echo.Context, amount, us } } - if limits.MaxAccountBalance > 0 { + if limits.MaxAccountBalance >= 0 { currentBalance, err := svc.CurrentUserBalance(c.Request().Context(), userId) if err != nil { svc.Logger.Errorj( @@ -326,20 +326,20 @@ func (svc *LndhubService) GetLimits(c echo.Context) (limits *Limits) { MaxReceiveAmount: svc.Config.MaxReceiveAmount, MaxAccountBalance: svc.Config.MaxAccountBalance, } - if val, ok := c.Get("MaxSendVolume").(int64); ok && val > 0 { - limits.MaxSendVolume = val + if val, ok := c.Get("MaxSendVolume").(*int64); ok && val != nil { + limits.MaxSendVolume = *val } - if val, ok := c.Get("MaxSendAmount").(int64); ok && val > 0 { - limits.MaxSendAmount = val + if val, ok := c.Get("MaxSendAmount").(*int64); ok && val != nil { + limits.MaxSendAmount = *val } - if val, ok := c.Get("MaxReceiveVolume").(int64); ok && val > 0 { - limits.MaxReceiveVolume = val + if val, ok := c.Get("MaxReceiveVolume").(*int64); ok && val != nil { + limits.MaxReceiveVolume = *val } - if val, ok := c.Get("MaxReceiveAmount").(int64); ok && val > 0 { - limits.MaxReceiveAmount = val + if val, ok := c.Get("MaxReceiveAmount").(*int64); ok && val != nil { + limits.MaxReceiveAmount = *val } - if val, ok := c.Get("MaxAccountBalance").(int64); ok && val > 0 { - limits.MaxAccountBalance = val + if val, ok := c.Get("MaxAccountBalance").(*int64); ok && val != nil { + limits.MaxAccountBalance = *val } return limits diff --git a/lib/tokens/jwt.go b/lib/tokens/jwt.go index b95c88c..bf92b13 100644 --- a/lib/tokens/jwt.go +++ b/lib/tokens/jwt.go @@ -15,13 +15,13 @@ import ( ) type jwtCustomClaims struct { - ID int64 `json:"id"` - IsRefresh bool `json:"isRefresh"` - MaxSendVolume int64 `json:"maxSendVolume"` - MaxSendAmount int64 `json:"maxSendAmount"` - MaxReceiveVolume int64 `json:"maxReceiveVolume"` - MaxReceiveAmount int64 `json:"maxReceiveAmount"` - MaxAccountBalance int64 `json:"maxAccountBalance"` + ID int64 `json:"id"` + IsRefresh bool `json:"isRefresh"` + MaxSendVolume *int64 `json:"maxSendVolume,omitempty"` + MaxSendAmount *int64 `json:"maxSendAmount,omitempty"` + MaxReceiveVolume *int64 `json:"maxReceiveVolume,omitempty"` + MaxReceiveAmount *int64 `json:"maxReceiveAmount,omitempty"` + MaxAccountBalance *int64 `json:"maxAccountBalance,omitempty"` jwt.StandardClaims }