Skip to content

Commit

Permalink
Merge pull request #464 from getAlby/task-refactor-checks
Browse files Browse the repository at this point in the history
chore: refactor payment checks
  • Loading branch information
kiwiidb authored Dec 7, 2023
2 parents c581250 + bd92e6f commit 786439b
Show file tree
Hide file tree
Showing 11 changed files with 171 additions and 146 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ vim .env # edit your config
+ `MAX_RECEIVE_AMOUNT`: (default: 0 = no limit) Set maximum amount (in satoshi) for which an invoice can be created
+ `MAX_SEND_AMOUNT`: (default: 0 = no limit) Set maximum amount (in satoshi) of an invoice that can be paid
+ `MAX_ACCOUNT_BALANCE`: (default: 0 = no limit) Set maximum balance (in satoshi) for each account
+ `MAX_VOLUME`: (default: 0 = no limit) Set maximum volume (in satoshi) for each account
+ `MAX_SEND_VOLUME`: (default: 0 = no limit) Set maximum volume (in satoshi) for sending for each account
+ `MAX_RECEIVE_VOLUME`: (default: 0 = no limit) Set maximum volume (in satoshi) for receiving for each account

### Macaroon

Expand Down
9 changes: 9 additions & 0 deletions controllers/addinvoice.ctrl.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ func AddInvoice(c echo.Context, svc *service.LndhubService, userID int64) error
return c.JSON(http.StatusBadRequest, responses.BadArgumentsError)
}

resp, err := svc.CheckIncomingPaymentAllowed(c.Request().Context(), amount, userID)
if err != nil {
return c.JSON(http.StatusInternalServerError, responses.GeneralServerError)
}
if resp != nil {
c.Logger().Errorf("Error: %v user_id:%v amount:%v", resp.Message, userID, amount)
return c.JSON(resp.HttpStatusCode, resp)
}

c.Logger().Infof("Adding invoice: user_id:%v memo:%s value:%v description_hash:%s", userID, body.Memo, amount, body.DescriptionHash)

invoice, errResp := svc.AddIncomingInvoice(c.Request().Context(), userID, amount, body.Memo, body.DescriptionHash)
Expand Down
15 changes: 4 additions & 11 deletions controllers/keysend.ctrl.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,20 +75,13 @@ func (controller *KeySendController) KeySend(c echo.Context) error {
})
}

resp, err := controller.svc.CheckPaymentAllowed(c.Request().Context(), lnPayReq, userID)
resp, err := controller.svc.CheckOutgoingPaymentAllowed(c.Request().Context(), lnPayReq, userID)
if err != nil {
c.Logger().Errorj(
log.JSON{
"message": "failed to check balance",
"error": err,
"lndhub_user_id": userID,
},
)
return c.JSON(http.StatusBadRequest, responses.BadArgumentsError)
return c.JSON(http.StatusInternalServerError, responses.GeneralServerError)
}
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, resp)
c.Logger().Errorf("Error: %v user_id:%v amount:%v", resp.Message, userID, lnPayReq.PayReq.NumSatoshis)
return c.JSON(resp.HttpStatusCode, resp)
}
invoice, errResp := controller.svc.AddOutgoingInvoice(c.Request().Context(), userID, "", lnPayReq)
if errResp != nil {
Expand Down
13 changes: 3 additions & 10 deletions controllers/payinvoice.ctrl.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,19 +90,12 @@ func (controller *PayInvoiceController) PayInvoice(c echo.Context) error {
lnPayReq.PayReq.NumSatoshis = amt
}

resp, err := controller.svc.CheckPaymentAllowed(c.Request().Context(), lnPayReq, userID)
resp, err := controller.svc.CheckOutgoingPaymentAllowed(c.Request().Context(), lnPayReq, userID)
if err != nil {
c.Logger().Errorj(
log.JSON{
"message": "error checking balance",
"error": err,
"lndhub_user_id": userID,
},
)
return c.JSON(http.StatusBadRequest, responses.GeneralServerError)
return c.JSON(http.StatusInternalServerError, responses.GeneralServerError)
}
if resp != nil {
c.Logger().Errorf("User does not have enough balance user_id:%v amount:%v", userID, lnPayReq.PayReq.NumSatoshis)
c.Logger().Errorf("Error: %v user_id:%v amount:%v", resp.Message, userID, lnPayReq.PayReq.NumSatoshis)
return c.JSON(http.StatusBadRequest, resp)
}

Expand Down
9 changes: 9 additions & 0 deletions controllers_v2/invoice.ctrl.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,15 @@ func (controller *InvoiceController) AddInvoice(c echo.Context) error {
return c.JSON(http.StatusBadRequest, responses.BadArgumentsError)
}

resp, err := controller.svc.CheckIncomingPaymentAllowed(c.Request().Context(), body.Amount, userID)
if err != nil {
return c.JSON(http.StatusInternalServerError, responses.GeneralServerError)
}
if resp != nil {
c.Logger().Errorf("Error: %v user_id:%v amount:%v", resp.Message, userID, body.Amount)
return c.JSON(resp.HttpStatusCode, resp)
}

c.Logger().Infof("Adding invoice: user_id:%v memo:%s value:%v description_hash:%s", userID, body.Description, body.Amount, body.DescriptionHash)

invoice, errResp := controller.svc.AddIncomingInvoice(c.Request().Context(), userID, body.Amount, body.Description, body.DescriptionHash)
Expand Down
11 changes: 5 additions & 6 deletions controllers_v2/keysend.ctrl.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,15 +169,14 @@ func (controller *KeySendController) checkKeysendPaymentAllowed(ctx context.Cont
},
Keysend: true,
}
resp, err := controller.svc.CheckPaymentAllowed(ctx, syntheticPayReq, userID)
if resp != nil {
controller.svc.Logger.Errorf("User does not have enough balance user_id:%v amount:%v", userID, syntheticPayReq.PayReq.NumSatoshis)
return resp
}
resp, err := controller.svc.CheckOutgoingPaymentAllowed(ctx, syntheticPayReq, userID)
if err != nil {
controller.svc.Logger.Error(err)
return &responses.GeneralServerError
}
if resp != nil {
controller.svc.Logger.Errorf("Error: %v user_id:%v amount:%v", resp.Message, userID, syntheticPayReq.PayReq.NumSatoshis)
return resp
}
return nil
}

Expand Down
15 changes: 4 additions & 11 deletions controllers_v2/payinvoice.ctrl.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,20 +98,13 @@ func (controller *PayInvoiceController) PayInvoice(c echo.Context) error {
}
lnPayReq.PayReq.NumSatoshis = amt
}
resp, err := controller.svc.CheckPaymentAllowed(c.Request().Context(), lnPayReq, userID)
resp, err := controller.svc.CheckOutgoingPaymentAllowed(c.Request().Context(), lnPayReq, userID)
if err != nil {
c.Logger().Errorj(
log.JSON{
"message": "error checking balance",
"error": err,
"lndhub_user_id": userID,
},
)
return err
return c.JSON(http.StatusBadRequest, responses.GeneralServerError)
}
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, resp)
c.Logger().Errorf("Error: %v user_id:%v amount:%v", resp.Message, userID, lnPayReq.PayReq.NumSatoshis)
return c.JSON(resp.HttpStatusCode, resp)
}
invoice, errResp := controller.svc.AddOutgoingInvoice(c.Request().Context(), userID, paymentRequest, lnPayReq)
if errResp != nil {
Expand Down
108 changes: 54 additions & 54 deletions integration_tests/internal_payment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,6 @@ func (suite *PaymentTestSuite) TestPaymentFeeReserve() {
func (suite *PaymentTestSuite) TestIncomingExceededChecks() {
//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)
Expand All @@ -146,52 +144,32 @@ func (suite *PaymentTestSuite) TestIncomingExceededChecks() {

//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 := 500
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,
suite.service.Config.MaxReceiveAmount = 21
rec := httptest.NewRecorder()
assert.NoError(suite.T(), json.NewEncoder(&buf).Encode(&ExpectedAddInvoiceRequestBody{
Amount: aliceFundingSats,
Memo: "memo",
}))
req := httptest.NewRequest(http.MethodPost, "/payinvoice", &buf)
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 volume check
//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.TooMuchVolumeError.Message, resp.Message)
assert.Equal(suite.T(), responses.ReceiveExceededError.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)
// remove volume and receive config and check if it works
suite.service.Config.MaxReceiveAmount = 0
invoiceResponse = suite.createAddInvoiceReq(aliceFundingSats, "integration test internal payment alice", suite.aliceToken)
err = suite.mlnd.mockPaidInvoice(invoiceResponse, 0, false, nil)
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)

suite.service.Config.MaxReceiveAmount = 21
rec = httptest.NewRecorder()
// add max account
suite.service.Config.MaxAccountBalance = 500
assert.NoError(suite.T(), json.NewEncoder(&buf).Encode(&ExpectedAddInvoiceRequestBody{
Amount: aliceFundingSats,
Memo: "memo",
Expand All @@ -200,23 +178,22 @@ func (suite *PaymentTestSuite) TestIncomingExceededChecks() {
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
//should fail because max balance 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)
assert.Equal(suite.T(), responses.BalanceExceededError.Message, resp.Message)

// remove volume and receive config and check if it works
suite.service.Config.MaxVolume = 0
suite.service.Config.MaxVolumePeriod = 0
suite.service.Config.MaxReceiveAmount = 0
//change the config back and add sats, it should work now
suite.service.Config.MaxAccountBalance = 0
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
suite.service.Config.MaxAccountBalance = 500
// add max receive volume
suite.service.Config.MaxReceiveVolume = 1999 // because the volume till here is 1000+500+500
suite.service.Config.MaxVolumePeriod = 2592000
assert.NoError(suite.T(), json.NewEncoder(&buf).Encode(&ExpectedAddInvoiceRequestBody{
Amount: aliceFundingSats,
Memo: "memo",
Expand All @@ -225,15 +202,16 @@ func (suite *PaymentTestSuite) TestIncomingExceededChecks() {
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 balance check
//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.BalanceExceededError.Message, resp.Message)
assert.Equal(suite.T(), responses.TooMuchVolumeError.Message, resp.Message)

//change the config back and add sats, it should work now
suite.service.Config.MaxAccountBalance = 0
//change the config back, it should work now
suite.service.Config.MaxReceiveVolume = 0
suite.service.Config.MaxVolumePeriod = 0
invoiceResponse = suite.createAddInvoiceReq(aliceFundingSats, "integration test internal payment alice", suite.aliceToken)
err = suite.mlnd.mockPaidInvoice(invoiceResponse, 0, false, nil)
assert.NoError(suite.T(), err)
Expand All @@ -255,7 +233,7 @@ func (suite *PaymentTestSuite) TestOutgoingExceededChecks() {
//try to make external payment
//which should fail
//create external invoice
externalSatRequested := 500
externalSatRequested := 400
externalInvoice := lnrpc.Invoice{
Memo: "integration tests: external pay from user",
Value: int64(externalSatRequested),
Expand All @@ -272,19 +250,17 @@ func (suite *PaymentTestSuite) TestOutgoingExceededChecks() {
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

//should fail because max send 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 = 2000
//should work now
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{
Expand All @@ -293,8 +269,32 @@ func (suite *PaymentTestSuite) TestOutgoingExceededChecks() {
suite.echo.ServeHTTP(rec, req)
assert.Equal(suite.T(), http.StatusOK, rec.Code)

suite.service.Config.MaxSendVolume = 100
suite.service.Config.MaxVolumePeriod = 2592000
//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 maximum 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 = 0
suite.service.Config.MaxSendVolume = 0
suite.service.Config.MaxVolumePeriod = 0
}

func (suite *PaymentTestSuite) TestInternalPayment() {
Expand Down
3 changes: 2 additions & 1 deletion lib/service/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +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:"0"` //0 means the volume check is disabled by default
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
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"`
Expand Down
Loading

0 comments on commit 786439b

Please sign in to comment.