From 3d8ecd425f0ec095f1a1ca3cd45ad65729da0055 Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Fri, 4 Mar 2022 11:08:27 +0100 Subject: [PATCH 01/14] keysend: low-level work and instructions --- controllers/keysend.go | 49 ++++++++++++++++++ .../20220304103000_keysend_invoice.up.sql | 1 + db/models/invoice.go | 1 + integration_tests/keysend_test.go | 12 +++++ lib/service/invoices.go | 3 ++ lib/service/ln.go | 50 +++++++++++++++++++ 6 files changed, 116 insertions(+) create mode 100644 controllers/keysend.go create mode 100644 db/migrations/20220304103000_keysend_invoice.up.sql create mode 100644 integration_tests/keysend_test.go diff --git a/controllers/keysend.go b/controllers/keysend.go new file mode 100644 index 0000000..dcc86a6 --- /dev/null +++ b/controllers/keysend.go @@ -0,0 +1,49 @@ +package controllers + +import ( + "fmt" + + "github.com/getAlby/lndhub.go/lib" + "github.com/getAlby/lndhub.go/lib/service" + "github.com/labstack/echo/v4" +) + +// KeySendController : Pay invoice controller struct +type KeySendController struct { + svc *service.LndhubService +} + +func NewKeySendController(svc *service.LndhubService) *KeySendController { + return &KeySendController{svc: svc} +} + +type KeySendRequestBody struct { + Amount int64 `json:"amount" validate:"required"` + Destination string `json:"destination" validate:"required"` + Memo string `json:"memo" validate:"omitempty"` +} + +type KeySendResponseBody struct { + RHash *lib.JavaScriptBuffer `json:"payment_hash,omitempty"` + Amount int64 `json:"num_satoshis,omitempty"` + Description string `json:"description,omitempty"` + Destination string `json:"destination,omitempty"` + DescriptionHashStr string `json:"description_hash,omitempty"` + PaymentError string `json:"payment_error,omitempty"` + PaymentPreimage *lib.JavaScriptBuffer `json:"payment_preimage,omitempty"` + PaymentRoute *service.Route `json:"route,omitempty"` +} + +// KeySend : Pay invoice Controller +func (controller *KeySendController) KeySend(c echo.Context) error { + /* + TODO: copy code from payinvoice.ctrl.go and modify where needed: + - do not decode the payment request because there is no payment request. + Instead, construct the lnrpc.PaymentRequest manually from the KeySendRequestBody. + - add outgoing invoice: same as payinvoice, make sure to set keysend=true + - do a balance check: same as payinvoice, in fact do this before doing anything else + - call svc.PayInvoice : same as payinvoice as long as keysend=true in Invoice + - response will be slightly different due to lack of payment request + */ + return fmt.Errorf("TODO") +} diff --git a/db/migrations/20220304103000_keysend_invoice.up.sql b/db/migrations/20220304103000_keysend_invoice.up.sql new file mode 100644 index 0000000..d7ca05d --- /dev/null +++ b/db/migrations/20220304103000_keysend_invoice.up.sql @@ -0,0 +1 @@ +alter table invoices add column keysend boolean; \ No newline at end of file diff --git a/db/models/invoice.go b/db/models/invoice.go index a117adf..d8419d6 100644 --- a/db/models/invoice.go +++ b/db/models/invoice.go @@ -21,6 +21,7 @@ type Invoice struct { RHash string `json:"r_hash"` Preimage string `json:"preimage" bun:",nullzero"` Internal bool `json:"internal" bun:",nullzero"` + KeySend bool `json:"keysend" bun:",nullzero"` State string `json:"state" bun:",default:'initialized'"` ErrorMessage string `json:"error_message" bun:",nullzero"` AddIndex uint64 `json:"add_index" bun:",nullzero"` diff --git a/integration_tests/keysend_test.go b/integration_tests/keysend_test.go new file mode 100644 index 0000000..f7bd03a --- /dev/null +++ b/integration_tests/keysend_test.go @@ -0,0 +1,12 @@ +package integration_tests + +func (suite *PaymentTestSuite) TestKeysendPayment() { + // destination pubkey strings: + // simnet-lnd-2: 025c1d5d1b4c983cc6350fc2d756fbb59b4dc365e45e87f8e3afe07e24013e8220 + // simnet-lnd-3: 03c7092d076f799ab18806743634b4c9bb34e351bdebc91d5b35963f3dc63ec5aa + // simnet-cln-1: 0242898f86064c2fd72de22059c947a83ba23e9d97aedeae7b6dba647123f1d71b + // (put this in utils) + // fund account, test making keysend payments to any of these nodes (lnd-2 and lnd-3 is fine) + // test making a keysend payment to a destination that does not exist + // test making a keysend payment with a memo that is waaaaaaay too long +} diff --git a/lib/service/invoices.go b/lib/service/invoices.go index 9b43d93..e8d6df1 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -98,6 +98,9 @@ func (svc *LndhubService) SendInternalPayment(ctx context.Context, invoice *mode } func (svc *LndhubService) SendPaymentSync(ctx context.Context, invoice *models.Invoice) (SendPaymentResponse, error) { + if invoice.KeySend { + return svc.KeySendPaymentSync(ctx, invoice) + } sendPaymentResponse := SendPaymentResponse{} // TODO: set dynamic fee limit feeLimit := lnrpc.FeeLimit{ diff --git a/lib/service/ln.go b/lib/service/ln.go index a9f609a..3d8554c 100644 --- a/lib/service/ln.go +++ b/lib/service/ln.go @@ -2,10 +2,60 @@ package service import ( "context" + "encoding/hex" + "errors" + "github.com/getAlby/lndhub.go/db/models" "github.com/lightningnetwork/lnd/lnrpc" ) +//https://github.com/hsjoberg/blixt-wallet/blob/9fcc56a7dc25237bc14b85e6490adb9e044c009c/src/utils/constants.ts#L5 +const ( + KEYSEND_CUSTOM_RECORD = 5482373484 + TLV_WHATSAT_MESSAGE = 34349334 + TLV_RECORD_NAME = 128100 +) + func (svc *LndhubService) GetInfo(ctx context.Context) (*lnrpc.GetInfoResponse, error) { return svc.LndClient.GetInfo(ctx, &lnrpc.GetInfoRequest{}) } + +func (svc *LndhubService) KeySendPaymentSync(ctx context.Context, invoice *models.Invoice) (result SendPaymentResponse, err error) { + sendPaymentResponse := SendPaymentResponse{} + // TODO: set dynamic fee limit + feeLimit := lnrpc.FeeLimit{ + Limit: &lnrpc.FeeLimit_Fixed{ + Fixed: 300, + }, + } + preImage := makePreimageHex() + // Prepare the LNRPC call + //See: https://github.com/hsjoberg/blixt-wallet/blob/9fcc56a7dc25237bc14b85e6490adb9e044c009c/src/lndmobile/index.ts#L251-L270 + sendPaymentRequest := lnrpc.SendRequest{ + Dest: []byte(invoice.DestinationPubkeyHex), + Amt: invoice.Amount, + FeeLimit: &feeLimit, + DestFeatures: []lnrpc.FeatureBit{lnrpc.FeatureBit_TLV_ONION_REQ}, + DestCustomRecords: map[uint64][]byte{KEYSEND_CUSTOM_RECORD: preImage, TLV_WHATSAT_MESSAGE: []byte(invoice.Memo)}, + } + + // Execute the payment + sendPaymentResult, err := svc.LndClient.SendPaymentSync(ctx, &sendPaymentRequest) + if err != nil { + return sendPaymentResponse, err + } + + // If there was a payment error we return an error + if sendPaymentResult.GetPaymentError() != "" || sendPaymentResult.GetPaymentPreimage() == nil { + return sendPaymentResponse, errors.New(sendPaymentResult.GetPaymentError()) + } + + preimage := sendPaymentResult.GetPaymentPreimage() + sendPaymentResponse.PaymentPreimage = preimage + sendPaymentResponse.PaymentPreimageStr = hex.EncodeToString(preimage[:]) + paymentHash := sendPaymentResult.GetPaymentHash() + sendPaymentResponse.PaymentHash = paymentHash + sendPaymentResponse.PaymentHashStr = hex.EncodeToString(paymentHash[:]) + sendPaymentResponse.PaymentRoute = &Route{TotalAmt: sendPaymentResult.PaymentRoute.TotalAmt, TotalFees: sendPaymentResult.PaymentRoute.TotalFees} + return sendPaymentResponse, nil +} From 7c938477a6b9fdd986080baeb295aeddd905c775 Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Fri, 4 Mar 2022 15:15:43 +0100 Subject: [PATCH 02/14] make keysend work on regtest --- integration_tests/keysend_test.go | 10 ++++++++++ lib/service/ln.go | 10 +++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/integration_tests/keysend_test.go b/integration_tests/keysend_test.go index f7bd03a..dd42f24 100644 --- a/integration_tests/keysend_test.go +++ b/integration_tests/keysend_test.go @@ -9,4 +9,14 @@ func (suite *PaymentTestSuite) TestKeysendPayment() { // fund account, test making keysend payments to any of these nodes (lnd-2 and lnd-3 is fine) // test making a keysend payment to a destination that does not exist // test making a keysend payment with a memo that is waaaaaaay too long + // inv, err := svc.KeySendPaymentSync(context.Background(), &models.Invoice{ + // ID: 0, + // Amount: 69420, + // Memo: "keysend integration test", + // DestinationPubkeyHex: "025c1d5d1b4c983cc6350fc2d756fbb59b4dc365e45e87f8e3afe07e24013e8220", + // Internal: false, + // KeySend: true, + // AddIndex: 0, + // }) + // fmt.Println(inv, err) } diff --git a/lib/service/ln.go b/lib/service/ln.go index 3d8554c..bce08a7 100644 --- a/lib/service/ln.go +++ b/lib/service/ln.go @@ -2,6 +2,7 @@ package service import ( "context" + "crypto/sha256" "encoding/hex" "errors" @@ -29,11 +30,18 @@ func (svc *LndhubService) KeySendPaymentSync(ctx context.Context, invoice *model }, } preImage := makePreimageHex() + 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 sendPaymentResponse, err + } sendPaymentRequest := lnrpc.SendRequest{ - Dest: []byte(invoice.DestinationPubkeyHex), + Dest: destBytes, Amt: invoice.Amount, + PaymentHash: pHash.Sum(nil), FeeLimit: &feeLimit, DestFeatures: []lnrpc.FeatureBit{lnrpc.FeatureBit_TLV_ONION_REQ}, DestCustomRecords: map[uint64][]byte{KEYSEND_CUSTOM_RECORD: preImage, TLV_WHATSAT_MESSAGE: []byte(invoice.Memo)}, From 8b077cd5ea00bae3d6143888aea8aabcac812694 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Fri, 4 Mar 2022 19:53:25 +0100 Subject: [PATCH 03/14] First key send endpoint impl based on pay invoice ctrl --- controllers/keysend.ctrl.go | 109 +++++++++++++++++++++++++++++++++ controllers/keysend.go | 49 --------------- controllers/payinvoice.ctrl.go | 2 +- lib/service/invoices.go | 3 +- 4 files changed, 112 insertions(+), 51 deletions(-) create mode 100644 controllers/keysend.ctrl.go delete mode 100644 controllers/keysend.go diff --git a/controllers/keysend.ctrl.go b/controllers/keysend.ctrl.go new file mode 100644 index 0000000..5b3d133 --- /dev/null +++ b/controllers/keysend.ctrl.go @@ -0,0 +1,109 @@ +package controllers + +import ( + "fmt" + "net/http" + + "github.com/getAlby/lndhub.go/lib" + "github.com/getAlby/lndhub.go/lib/responses" + "github.com/getAlby/lndhub.go/lib/service" + "github.com/getsentry/sentry-go" + "github.com/labstack/echo/v4" + "github.com/lightningnetwork/lnd/lnrpc" +) + +// KeySendController : Pay invoice controller struct +type KeySendController struct { + svc *service.LndhubService +} + +func NewKeySendController(svc *service.LndhubService) *KeySendController { + return &KeySendController{svc: svc} +} + +type KeySendRequestBody struct { + Amount int64 `json:"amount" validate:"required"` + Destination string `json:"destination" validate:"required"` + Memo string `json:"memo" validate:"omitempty"` +} + +type KeySendResponseBody struct { + RHash *lib.JavaScriptBuffer `json:"payment_hash,omitempty"` + Amount int64 `json:"num_satoshis,omitempty"` + Description string `json:"description,omitempty"` + Destination string `json:"destination,omitempty"` + DescriptionHashStr string `json:"description_hash,omitempty"` + PaymentError string `json:"payment_error,omitempty"` + PaymentPreimage *lib.JavaScriptBuffer `json:"payment_preimage,omitempty"` + PaymentRoute *service.Route `json:"route,omitempty"` +} + +// KeySend : Pay invoice Controller +func (controller *KeySendController) KeySend(c echo.Context) error { + /* + TODO: copy code from payinvoice.ctrl.go and modify where needed: + - do not decode the payment request because there is no payment request. + Instead, construct the lnrpc.PaymentRequest manually from the KeySendRequestBody. + - add outgoing invoice: same as payinvoice, make sure to set keysend=true + - do a balance check: same as payinvoice, in fact do this before doing anything else + - call svc.PayInvoice : same as payinvoice as long as keysend=true in Invoice + - response will be slightly different due to lack of payment request + */ + + userID := c.Get("UserID").(int64) + reqBody := KeySendRequestBody{} + if err := c.Bind(&reqBody); err != nil { + c.Logger().Errorf("Failed to load payinvoice request body: %v", err) + return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) + } + + if err := c.Validate(&reqBody); err != nil { + c.Logger().Errorf("Invalid payinvoice request body: %v", err) + return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) + } + + paymentRequest := &lnrpc.PayReq{ + Destination: reqBody.Destination, + NumSatoshis: reqBody.Amount, + Description: reqBody.Memo, + } + + invoice, err := controller.svc.AddOutgoingInvoice(c.Request().Context(), userID, "", paymentRequest, true) + if err != nil { + return err + } + + currentBalance, err := controller.svc.CurrentUserBalance(c.Request().Context(), userID) + if err != nil { + return err + } + + if currentBalance < invoice.Amount { + c.Logger().Errorf("User does not have enough balance invoice_id=%v user_id=%v balance=%v amount=%v", invoice.ID, userID, currentBalance, invoice.Amount) + + return c.JSON(http.StatusBadRequest, responses.NotEnoughBalanceError) + } + + _, err = controller.svc.PayInvoice(c.Request().Context(), invoice) + if err != nil { + c.Logger().Errorf("Payment failed: %v", err) + sentry.CaptureException(err) + return c.JSON(http.StatusBadRequest, echo.Map{ + "error": true, + "code": 10, + "message": fmt.Sprintf("Payment failed. Does the receiver have enough inbound capacity? (%v)", err), + }) + } + responseBody := &PayInvoiceResponseBody{} + // responseBody.RHash = &lib.JavaScriptBuffer{Data: sendPaymentResponse.PaymentHash} + // responseBody.PaymentRequest = paymentRequest + // responseBody.PayReq = paymentRequest + // responseBody.Amount = invoice.Amount + // responseBody.Description = invoice.Memo + // responseBody.DescriptionHashStr = invoice.DescriptionHash + // responseBody.PaymentError = sendPaymentResponse.PaymentError + // responseBody.PaymentPreimage = &lib.JavaScriptBuffer{Data: sendPaymentResponse.PaymentPreimage} + // responseBody.PaymentRoute = sendPaymentResponse.PaymentRoute + + return c.JSON(http.StatusOK, responseBody) +} diff --git a/controllers/keysend.go b/controllers/keysend.go deleted file mode 100644 index dcc86a6..0000000 --- a/controllers/keysend.go +++ /dev/null @@ -1,49 +0,0 @@ -package controllers - -import ( - "fmt" - - "github.com/getAlby/lndhub.go/lib" - "github.com/getAlby/lndhub.go/lib/service" - "github.com/labstack/echo/v4" -) - -// KeySendController : Pay invoice controller struct -type KeySendController struct { - svc *service.LndhubService -} - -func NewKeySendController(svc *service.LndhubService) *KeySendController { - return &KeySendController{svc: svc} -} - -type KeySendRequestBody struct { - Amount int64 `json:"amount" validate:"required"` - Destination string `json:"destination" validate:"required"` - Memo string `json:"memo" validate:"omitempty"` -} - -type KeySendResponseBody struct { - RHash *lib.JavaScriptBuffer `json:"payment_hash,omitempty"` - Amount int64 `json:"num_satoshis,omitempty"` - Description string `json:"description,omitempty"` - Destination string `json:"destination,omitempty"` - DescriptionHashStr string `json:"description_hash,omitempty"` - PaymentError string `json:"payment_error,omitempty"` - PaymentPreimage *lib.JavaScriptBuffer `json:"payment_preimage,omitempty"` - PaymentRoute *service.Route `json:"route,omitempty"` -} - -// KeySend : Pay invoice Controller -func (controller *KeySendController) KeySend(c echo.Context) error { - /* - TODO: copy code from payinvoice.ctrl.go and modify where needed: - - do not decode the payment request because there is no payment request. - Instead, construct the lnrpc.PaymentRequest manually from the KeySendRequestBody. - - add outgoing invoice: same as payinvoice, make sure to set keysend=true - - do a balance check: same as payinvoice, in fact do this before doing anything else - - call svc.PayInvoice : same as payinvoice as long as keysend=true in Invoice - - response will be slightly different due to lack of payment request - */ - return fmt.Errorf("TODO") -} diff --git a/controllers/payinvoice.ctrl.go b/controllers/payinvoice.ctrl.go index 36c4e7e..8501423 100644 --- a/controllers/payinvoice.ctrl.go +++ b/controllers/payinvoice.ctrl.go @@ -69,7 +69,7 @@ func (controller *PayInvoiceController) PayInvoice(c echo.Context) error { } */ - invoice, err := controller.svc.AddOutgoingInvoice(c.Request().Context(), userID, paymentRequest, decodedPaymentRequest) + invoice, err := controller.svc.AddOutgoingInvoice(c.Request().Context(), userID, paymentRequest, decodedPaymentRequest, false) if err != nil { return err } diff --git a/lib/service/invoices.go b/lib/service/invoices.go index e8d6df1..b4acc5e 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -238,7 +238,7 @@ func (svc *LndhubService) HandleSuccessfulPayment(ctx context.Context, invoice * return err } -func (svc *LndhubService) AddOutgoingInvoice(ctx context.Context, userID int64, paymentRequest string, decodedInvoice *lnrpc.PayReq) (*models.Invoice, error) { +func (svc *LndhubService) AddOutgoingInvoice(ctx context.Context, userID int64, paymentRequest string, decodedInvoice *lnrpc.PayReq, keysend bool) (*models.Invoice, error) { // Initialize new DB invoice invoice := models.Invoice{ Type: common.InvoiceTypeOutgoing, @@ -250,6 +250,7 @@ func (svc *LndhubService) AddOutgoingInvoice(ctx context.Context, userID int64, DestinationPubkeyHex: decodedInvoice.Destination, DescriptionHash: decodedInvoice.DescriptionHash, Memo: decodedInvoice.Description, + KeySend: keysend, ExpiresAt: bun.NullTime{Time: time.Unix(decodedInvoice.Timestamp, 0).Add(time.Duration(decodedInvoice.Expiry) * time.Second)}, } From dd600af5b2a357d0a6dbb101d127b6033c2a46b1 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Fri, 4 Mar 2022 20:05:59 +0100 Subject: [PATCH 04/14] Refactor send payment sync a bit to reduce code duplication --- lib/service/invoices.go | 61 +++++++++++++++++++++++++++++------------ lib/service/ln.go | 51 ---------------------------------- 2 files changed, 43 insertions(+), 69 deletions(-) diff --git a/lib/service/invoices.go b/lib/service/invoices.go index b4acc5e..330dc94 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -2,6 +2,7 @@ package service import ( "context" + "crypto/sha256" "encoding/hex" "errors" "math/rand" @@ -98,29 +99,15 @@ func (svc *LndhubService) SendInternalPayment(ctx context.Context, invoice *mode } func (svc *LndhubService) SendPaymentSync(ctx context.Context, invoice *models.Invoice) (SendPaymentResponse, error) { - if invoice.KeySend { - return svc.KeySendPaymentSync(ctx, invoice) - } sendPaymentResponse := SendPaymentResponse{} - // TODO: set dynamic fee limit - feeLimit := lnrpc.FeeLimit{ - //Limit: &lnrpc.FeeLimit_Percent{ - // Percent: 2, - //}, - Limit: &lnrpc.FeeLimit_Fixed{ - Fixed: 300, - }, - } - // Prepare the LNRPC call - sendPaymentRequest := lnrpc.SendRequest{ - PaymentRequest: invoice.PaymentRequest, - Amt: invoice.Amount, - FeeLimit: &feeLimit, + sendPaymentRequest, err := createLnRpcSendRequest(invoice) + if err != nil { + return sendPaymentResponse, err } // Execute the payment - sendPaymentResult, err := svc.LndClient.SendPaymentSync(ctx, &sendPaymentRequest) + sendPaymentResult, err := svc.LndClient.SendPaymentSync(ctx, sendPaymentRequest) if err != nil { return sendPaymentResponse, err } @@ -140,6 +127,44 @@ func (svc *LndhubService) SendPaymentSync(ctx context.Context, invoice *models.I return sendPaymentResponse, nil } +func createLnRpcSendRequest(invoice *models.Invoice) (*lnrpc.SendRequest, error) { + // TODO: set dynamic fee limit + feeLimit := lnrpc.FeeLimit{ + //Limit: &lnrpc.FeeLimit_Percent{ + // Percent: 2, + //}, + Limit: &lnrpc.FeeLimit_Fixed{ + Fixed: 300, + }, + } + + if !invoice.KeySend { + return &lnrpc.SendRequest{ + PaymentRequest: invoice.PaymentRequest, + Amt: invoice.Amount, + FeeLimit: &feeLimit, + }, nil + } + + preImage := makePreimageHex() + 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 + } + return &lnrpc.SendRequest{ + Dest: destBytes, + Amt: invoice.Amount, + PaymentHash: pHash.Sum(nil), + FeeLimit: &feeLimit, + DestFeatures: []lnrpc.FeatureBit{lnrpc.FeatureBit_TLV_ONION_REQ}, + DestCustomRecords: map[uint64][]byte{KEYSEND_CUSTOM_RECORD: preImage, TLV_WHATSAT_MESSAGE: []byte(invoice.Memo)}, + }, nil +} + func (svc *LndhubService) PayInvoice(ctx context.Context, invoice *models.Invoice) (*SendPaymentResponse, error) { userId := invoice.UserID diff --git a/lib/service/ln.go b/lib/service/ln.go index bce08a7..f139e2b 100644 --- a/lib/service/ln.go +++ b/lib/service/ln.go @@ -2,11 +2,7 @@ package service import ( "context" - "crypto/sha256" - "encoding/hex" - "errors" - "github.com/getAlby/lndhub.go/db/models" "github.com/lightningnetwork/lnd/lnrpc" ) @@ -20,50 +16,3 @@ const ( func (svc *LndhubService) GetInfo(ctx context.Context) (*lnrpc.GetInfoResponse, error) { return svc.LndClient.GetInfo(ctx, &lnrpc.GetInfoRequest{}) } - -func (svc *LndhubService) KeySendPaymentSync(ctx context.Context, invoice *models.Invoice) (result SendPaymentResponse, err error) { - sendPaymentResponse := SendPaymentResponse{} - // TODO: set dynamic fee limit - feeLimit := lnrpc.FeeLimit{ - Limit: &lnrpc.FeeLimit_Fixed{ - Fixed: 300, - }, - } - preImage := makePreimageHex() - 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 sendPaymentResponse, err - } - sendPaymentRequest := lnrpc.SendRequest{ - Dest: destBytes, - Amt: invoice.Amount, - PaymentHash: pHash.Sum(nil), - FeeLimit: &feeLimit, - DestFeatures: []lnrpc.FeatureBit{lnrpc.FeatureBit_TLV_ONION_REQ}, - DestCustomRecords: map[uint64][]byte{KEYSEND_CUSTOM_RECORD: preImage, TLV_WHATSAT_MESSAGE: []byte(invoice.Memo)}, - } - - // Execute the payment - sendPaymentResult, err := svc.LndClient.SendPaymentSync(ctx, &sendPaymentRequest) - if err != nil { - return sendPaymentResponse, err - } - - // If there was a payment error we return an error - if sendPaymentResult.GetPaymentError() != "" || sendPaymentResult.GetPaymentPreimage() == nil { - return sendPaymentResponse, errors.New(sendPaymentResult.GetPaymentError()) - } - - preimage := sendPaymentResult.GetPaymentPreimage() - sendPaymentResponse.PaymentPreimage = preimage - sendPaymentResponse.PaymentPreimageStr = hex.EncodeToString(preimage[:]) - paymentHash := sendPaymentResult.GetPaymentHash() - sendPaymentResponse.PaymentHash = paymentHash - sendPaymentResponse.PaymentHashStr = hex.EncodeToString(paymentHash[:]) - sendPaymentResponse.PaymentRoute = &Route{TotalAmt: sendPaymentResult.PaymentRoute.TotalAmt, TotalFees: sendPaymentResult.PaymentRoute.TotalFees} - return sendPaymentResponse, nil -} From b82deb3835217785cbac8c456912e588f93c888a Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Tue, 8 Mar 2022 01:08:57 +0100 Subject: [PATCH 05/14] Register endpoint --- main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/main.go b/main.go index c67ff7e..f888aa5 100644 --- a/main.go +++ b/main.go @@ -134,6 +134,7 @@ func main() { secured.GET("/checkpayment/:payment_hash", controllers.NewCheckPaymentController(svc).CheckPayment) secured.GET("/balance", controllers.NewBalanceController(svc).Balance) secured.GET("/getinfo", controllers.NewGetInfoController(svc).GetInfo) + secured.GET("/keysend", controllers.NewKeySendController(svc).KeySend) // These endpoints are currently not supported and we return a blank response for backwards compatibility blankController := controllers.NewBlankController(svc) From c6ba66122782684afc65ee7fa81e68e45a63b19a Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Tue, 8 Mar 2022 01:09:11 +0100 Subject: [PATCH 06/14] Fix key send response body --- controllers/keysend.ctrl.go | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/controllers/keysend.ctrl.go b/controllers/keysend.ctrl.go index 5b3d133..d31cd14 100644 --- a/controllers/keysend.ctrl.go +++ b/controllers/keysend.ctrl.go @@ -84,7 +84,7 @@ func (controller *KeySendController) KeySend(c echo.Context) error { return c.JSON(http.StatusBadRequest, responses.NotEnoughBalanceError) } - _, err = controller.svc.PayInvoice(c.Request().Context(), invoice) + sendPaymentResponse, err := controller.svc.PayInvoice(c.Request().Context(), invoice) if err != nil { c.Logger().Errorf("Payment failed: %v", err) sentry.CaptureException(err) @@ -95,15 +95,13 @@ func (controller *KeySendController) KeySend(c echo.Context) error { }) } responseBody := &PayInvoiceResponseBody{} - // responseBody.RHash = &lib.JavaScriptBuffer{Data: sendPaymentResponse.PaymentHash} - // responseBody.PaymentRequest = paymentRequest - // responseBody.PayReq = paymentRequest - // responseBody.Amount = invoice.Amount - // responseBody.Description = invoice.Memo - // responseBody.DescriptionHashStr = invoice.DescriptionHash - // responseBody.PaymentError = sendPaymentResponse.PaymentError - // responseBody.PaymentPreimage = &lib.JavaScriptBuffer{Data: sendPaymentResponse.PaymentPreimage} - // responseBody.PaymentRoute = sendPaymentResponse.PaymentRoute + responseBody.RHash = &lib.JavaScriptBuffer{Data: sendPaymentResponse.PaymentHash} + responseBody.Amount = invoice.Amount + responseBody.Description = invoice.Memo + responseBody.DescriptionHashStr = invoice.DescriptionHash + responseBody.PaymentError = sendPaymentResponse.PaymentError + responseBody.PaymentPreimage = &lib.JavaScriptBuffer{Data: sendPaymentResponse.PaymentPreimage} + responseBody.PaymentRoute = sendPaymentResponse.PaymentRoute return c.JSON(http.StatusOK, responseBody) } From 6dc4e608fba609198f7368a890f11d1e9d4cf576 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Tue, 8 Mar 2022 01:09:43 +0100 Subject: [PATCH 07/14] Fix invoice keysend spelling --- db/models/invoice.go | 2 +- lib/service/invoices.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/db/models/invoice.go b/db/models/invoice.go index d8419d6..0679240 100644 --- a/db/models/invoice.go +++ b/db/models/invoice.go @@ -21,7 +21,7 @@ type Invoice struct { RHash string `json:"r_hash"` Preimage string `json:"preimage" bun:",nullzero"` Internal bool `json:"internal" bun:",nullzero"` - KeySend bool `json:"keysend" bun:",nullzero"` + Keysend bool `json:"keysend" bun:",nullzero"` State string `json:"state" bun:",default:'initialized'"` ErrorMessage string `json:"error_message" bun:",nullzero"` AddIndex uint64 `json:"add_index" bun:",nullzero"` diff --git a/lib/service/invoices.go b/lib/service/invoices.go index 330dc94..f39902e 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -138,7 +138,7 @@ func createLnRpcSendRequest(invoice *models.Invoice) (*lnrpc.SendRequest, error) }, } - if !invoice.KeySend { + if !invoice.Keysend { return &lnrpc.SendRequest{ PaymentRequest: invoice.PaymentRequest, Amt: invoice.Amount, @@ -275,7 +275,7 @@ func (svc *LndhubService) AddOutgoingInvoice(ctx context.Context, userID int64, DestinationPubkeyHex: decodedInvoice.Destination, DescriptionHash: decodedInvoice.DescriptionHash, Memo: decodedInvoice.Description, - KeySend: keysend, + Keysend: keysend, ExpiresAt: bun.NullTime{Time: time.Unix(decodedInvoice.Timestamp, 0).Add(time.Duration(decodedInvoice.Expiry) * time.Second)}, } From ba7c045729189ba052439734cb925b22030b0b60 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Tue, 8 Mar 2022 01:11:02 +0100 Subject: [PATCH 08/14] Add keysend tests --- integration_tests/keysend_test.go | 142 ++++++++++++++++++++++++++---- integration_tests/util.go | 41 +++++++++ 2 files changed, 164 insertions(+), 19 deletions(-) diff --git a/integration_tests/keysend_test.go b/integration_tests/keysend_test.go index dd42f24..a60cdaa 100644 --- a/integration_tests/keysend_test.go +++ b/integration_tests/keysend_test.go @@ -1,22 +1,126 @@ package integration_tests -func (suite *PaymentTestSuite) TestKeysendPayment() { - // destination pubkey strings: - // simnet-lnd-2: 025c1d5d1b4c983cc6350fc2d756fbb59b4dc365e45e87f8e3afe07e24013e8220 - // simnet-lnd-3: 03c7092d076f799ab18806743634b4c9bb34e351bdebc91d5b35963f3dc63ec5aa - // simnet-cln-1: 0242898f86064c2fd72de22059c947a83ba23e9d97aedeae7b6dba647123f1d71b - // (put this in utils) - // fund account, test making keysend payments to any of these nodes (lnd-2 and lnd-3 is fine) - // test making a keysend payment to a destination that does not exist - // test making a keysend payment with a memo that is waaaaaaay too long - // inv, err := svc.KeySendPaymentSync(context.Background(), &models.Invoice{ - // ID: 0, - // Amount: 69420, - // Memo: "keysend integration test", - // DestinationPubkeyHex: "025c1d5d1b4c983cc6350fc2d756fbb59b4dc365e45e87f8e3afe07e24013e8220", - // Internal: false, - // KeySend: true, - // AddIndex: 0, - // }) - // fmt.Println(inv, err) +import ( + "context" + "fmt" + "log" + "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 KeySendTestSuite struct { + TestSuite + fundingClient *lnd.LNDWrapper + service *service.LndhubService + aliceLogin controllers.CreateUserResponseBody + aliceToken string + bobLogin controllers.CreateUserResponseBody + bobToken string + invoiceUpdateSubCancelFn context.CancelFunc +} + +func (suite *KeySendTestSuite) 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, 2) + 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(), 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) + suite.echo.POST("/keysend", controllers.NewKeySendController(suite.service).KeySend) +} + +func (suite *KeySendTestSuite) TearDownSuite() { + suite.invoiceUpdateSubCancelFn() +} + +func (suite *KeySendTestSuite) TestKeysendPayment() { + 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) + + suite.createKeySendReq(int64(externalSatRequested), "key send test", simnetLnd3PubKey, suite.aliceToken) + + // 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(aliceFundingSats)-int64(externalSatRequested), aliceBalance) +} + +func (suite *KeySendTestSuite) TestKeysendPaymentNonExistendDestination() { + 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) + + suite.createKeySendReqError(int64(externalSatRequested), "key send test", "12345", suite.aliceToken) +} + +func TestKeySendTestSuite(t *testing.T) { + suite.Run(t, new(KeySendTestSuite)) } diff --git a/integration_tests/util.go b/integration_tests/util.go index 6a15abc..6361391 100644 --- a/integration_tests/util.go +++ b/integration_tests/util.go @@ -29,6 +29,9 @@ const ( lnd1RegtestMacaroonHex = "0201036c6e6402f801030a10e2133a1cac2c5b4d56e44e32dc64c8551201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620c4f9783e0873fa50a2091806f5ebb919c5dc432e33800b401463ada6485df0ed" lnd2RegtestAddress = "rpc.lnd2.regtest.getalby.com:443" lnd2RegtestMacaroonHex = "0201036C6E6402F801030A101782922F4358E80655920FC7A7C3E9291201301A160A0761646472657373120472656164120577726974651A130A04696E666F120472656164120577726974651A170A08696E766F69636573120472656164120577726974651A210A086D616361726F6F6E120867656E6572617465120472656164120577726974651A160A076D657373616765120472656164120577726974651A170A086F6666636861696E120472656164120577726974651A160A076F6E636861696E120472656164120577726974651A140A057065657273120472656164120577726974651A180A067369676E6572120867656E657261746512047265616400000620628FFB2938C8540DD3AA5E578D9B43456835FAA176E175FFD4F9FBAE540E3BE9" + simnetLnd1PubKey = "0242898f86064c2fd72de22059c947a83ba23e9d97aedeae7b6dba647123f1d71b" + simnetLnd2PubKey = "025c1d5d1b4c983cc6350fc2d756fbb59b4dc365e45e87f8e3afe07e24013e8220" + simnetLnd3PubKey = "03c7092d076f799ab18806743634b4c9bb34e351bdebc91d5b35963f3dc63ec5aa" ) func LndHubTestServiceInit(lndClientMock lnd.LightningClientWrapper) (svc *service.LndhubService, err error) { @@ -150,6 +153,44 @@ func (suite *TestSuite) createAddInvoiceReq(amt int, memo, token string) *contro return invoiceResponse } +func (suite *TestSuite) createKeySendReq(amount int64, memo, destination, token string) *controllers.KeySendResponseBody { + rec := httptest.NewRecorder() + var buf bytes.Buffer + assert.NoError(suite.T(), json.NewEncoder(&buf).Encode(&controllers.KeySendRequestBody{ + Amount: amount, + Destination: destination, + Memo: memo, + })) + req := httptest.NewRequest(http.MethodPost, "/keysend", &buf) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + suite.echo.ServeHTTP(rec, req) + + keySendResponse := &controllers.KeySendResponseBody{} + assert.Equal(suite.T(), http.StatusOK, rec.Code) + assert.NoError(suite.T(), json.NewDecoder(rec.Body).Decode(keySendResponse)) + return keySendResponse +} + +func (suite *TestSuite) createKeySendReqError(amount int64, memo, destination, token string) *responses.ErrorResponse { + rec := httptest.NewRecorder() + var buf bytes.Buffer + assert.NoError(suite.T(), json.NewEncoder(&buf).Encode(&controllers.KeySendRequestBody{ + Amount: amount, + Destination: destination, + Memo: memo, + })) + req := httptest.NewRequest(http.MethodPost, "/keysend", &buf) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + 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)) + return errorResponse +} + func (suite *TestSuite) createPayInvoiceReq(payReq string, token string) *controllers.PayInvoiceResponseBody { rec := httptest.NewRecorder() var buf bytes.Buffer From c6b4c3028c13dfba3d1d516e5f9d02a1da25d41f Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Tue, 8 Mar 2022 01:15:37 +0100 Subject: [PATCH 09/14] Remove todo and cleanup --- controllers/keysend.ctrl.go | 14 ++------------ integration_tests/keysend_test.go | 12 ++++-------- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/controllers/keysend.ctrl.go b/controllers/keysend.ctrl.go index d31cd14..ab28881 100644 --- a/controllers/keysend.ctrl.go +++ b/controllers/keysend.ctrl.go @@ -40,25 +40,15 @@ type KeySendResponseBody struct { // KeySend : Pay invoice Controller func (controller *KeySendController) KeySend(c echo.Context) error { - /* - TODO: copy code from payinvoice.ctrl.go and modify where needed: - - do not decode the payment request because there is no payment request. - Instead, construct the lnrpc.PaymentRequest manually from the KeySendRequestBody. - - add outgoing invoice: same as payinvoice, make sure to set keysend=true - - do a balance check: same as payinvoice, in fact do this before doing anything else - - call svc.PayInvoice : same as payinvoice as long as keysend=true in Invoice - - response will be slightly different due to lack of payment request - */ - userID := c.Get("UserID").(int64) reqBody := KeySendRequestBody{} if err := c.Bind(&reqBody); err != nil { - c.Logger().Errorf("Failed to load payinvoice request body: %v", err) + c.Logger().Errorf("Failed to load keysend request body: %v", err) return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) } if err := c.Validate(&reqBody); err != nil { - c.Logger().Errorf("Invalid payinvoice request body: %v", err) + c.Logger().Errorf("Invalid keysend request body: %v", err) return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) } diff --git a/integration_tests/keysend_test.go b/integration_tests/keysend_test.go index a60cdaa..d834a27 100644 --- a/integration_tests/keysend_test.go +++ b/integration_tests/keysend_test.go @@ -26,8 +26,6 @@ type KeySendTestSuite struct { service *service.LndhubService aliceLogin controllers.CreateUserResponseBody aliceToken string - bobLogin controllers.CreateUserResponseBody - bobToken string invoiceUpdateSubCancelFn context.CancelFunc } @@ -45,7 +43,7 @@ func (suite *KeySendTestSuite) 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) } @@ -60,12 +58,10 @@ func (suite *KeySendTestSuite) 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)) + assert.Equal(suite.T(), 1, len(users)) + assert.Equal(suite.T(), 1, 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) @@ -103,7 +99,7 @@ func (suite *KeySendTestSuite) TestKeysendPayment() { assert.Equal(suite.T(), int64(aliceFundingSats)-int64(externalSatRequested), aliceBalance) } -func (suite *KeySendTestSuite) TestKeysendPaymentNonExistendDestination() { +func (suite *KeySendTestSuite) TestKeysendPaymentNonExistentDestination() { aliceFundingSats := 1000 externalSatRequested := 500 //fund alice account From 03e44fac58cefa81af4aea08ee27c094f9fdfc89 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Tue, 8 Mar 2022 01:21:28 +0100 Subject: [PATCH 10/14] Fix keysend response body type --- controllers/keysend.ctrl.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/controllers/keysend.ctrl.go b/controllers/keysend.ctrl.go index ab28881..50d25ec 100644 --- a/controllers/keysend.ctrl.go +++ b/controllers/keysend.ctrl.go @@ -84,9 +84,10 @@ func (controller *KeySendController) KeySend(c echo.Context) error { "message": fmt.Sprintf("Payment failed. Does the receiver have enough inbound capacity? (%v)", err), }) } - responseBody := &PayInvoiceResponseBody{} + responseBody := &KeySendResponseBody{} responseBody.RHash = &lib.JavaScriptBuffer{Data: sendPaymentResponse.PaymentHash} responseBody.Amount = invoice.Amount + responseBody.Destination = invoice.DestinationPubkeyHex responseBody.Description = invoice.Memo responseBody.DescriptionHashStr = invoice.DescriptionHash responseBody.PaymentError = sendPaymentResponse.PaymentError From 3a12f908c4ebed071a2f42ae3b1ddee8d01ef476 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Tue, 8 Mar 2022 01:29:01 +0100 Subject: [PATCH 11/14] Cleanup keysend ctrl --- controllers/keysend.ctrl.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/controllers/keysend.ctrl.go b/controllers/keysend.ctrl.go index 50d25ec..29acc67 100644 --- a/controllers/keysend.ctrl.go +++ b/controllers/keysend.ctrl.go @@ -12,7 +12,7 @@ import ( "github.com/lightningnetwork/lnd/lnrpc" ) -// KeySendController : Pay invoice controller struct +// KeySendController : Key send controller struct type KeySendController struct { svc *service.LndhubService } @@ -38,7 +38,7 @@ type KeySendResponseBody struct { PaymentRoute *service.Route `json:"route,omitempty"` } -// KeySend : Pay invoice Controller +// KeySend : Key send Controller func (controller *KeySendController) KeySend(c echo.Context) error { userID := c.Get("UserID").(int64) reqBody := KeySendRequestBody{} @@ -70,7 +70,6 @@ func (controller *KeySendController) KeySend(c echo.Context) error { if currentBalance < invoice.Amount { c.Logger().Errorf("User does not have enough balance invoice_id=%v user_id=%v balance=%v amount=%v", invoice.ID, userID, currentBalance, invoice.Amount) - return c.JSON(http.StatusBadRequest, responses.NotEnoughBalanceError) } @@ -84,6 +83,7 @@ func (controller *KeySendController) KeySend(c echo.Context) error { "message": fmt.Sprintf("Payment failed. Does the receiver have enough inbound capacity? (%v)", err), }) } + responseBody := &KeySendResponseBody{} responseBody.RHash = &lib.JavaScriptBuffer{Data: sendPaymentResponse.PaymentHash} responseBody.Amount = invoice.Amount From 2548de40a52b703c2b9ec25d2709f7a27c828813 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Tue, 8 Mar 2022 12:30:43 +0100 Subject: [PATCH 12/14] Change keysend endpoint method to POST --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index f888aa5..19b5843 100644 --- a/main.go +++ b/main.go @@ -134,7 +134,7 @@ func main() { secured.GET("/checkpayment/:payment_hash", controllers.NewCheckPaymentController(svc).CheckPayment) secured.GET("/balance", controllers.NewBalanceController(svc).Balance) secured.GET("/getinfo", controllers.NewGetInfoController(svc).GetInfo) - secured.GET("/keysend", controllers.NewKeySendController(svc).KeySend) + secured.POST("/keysend", controllers.NewKeySendController(svc).KeySend) // These endpoints are currently not supported and we return a blank response for backwards compatibility blankController := controllers.NewBlankController(svc) From f7b2ea76d6f90f037a84de93171c613b37841024 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Tue, 8 Mar 2022 12:32:02 +0100 Subject: [PATCH 13/14] Group payreq and keysend flag into struct --- controllers/keysend.ctrl.go | 14 +++++++++----- controllers/payinvoice.ctrl.go | 8 +++++++- lib/service/invoices.go | 17 +++++++++-------- lnd/lnd.go | 5 +++++ 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/controllers/keysend.ctrl.go b/controllers/keysend.ctrl.go index 29acc67..e3fb1bc 100644 --- a/controllers/keysend.ctrl.go +++ b/controllers/keysend.ctrl.go @@ -7,6 +7,7 @@ import ( "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/lnd" "github.com/getsentry/sentry-go" "github.com/labstack/echo/v4" "github.com/lightningnetwork/lnd/lnrpc" @@ -52,13 +53,16 @@ func (controller *KeySendController) KeySend(c echo.Context) error { return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) } - paymentRequest := &lnrpc.PayReq{ - Destination: reqBody.Destination, - NumSatoshis: reqBody.Amount, - Description: reqBody.Memo, + lndPayReq := &lnd.LNDPayReq{ + PayReq: &lnrpc.PayReq{ + Destination: reqBody.Destination, + NumSatoshis: reqBody.Amount, + Description: reqBody.Memo, + }, + Keysend: true, } - invoice, err := controller.svc.AddOutgoingInvoice(c.Request().Context(), userID, "", paymentRequest, true) + invoice, err := controller.svc.AddOutgoingInvoice(c.Request().Context(), userID, "", lndPayReq) if err != nil { return err } diff --git a/controllers/payinvoice.ctrl.go b/controllers/payinvoice.ctrl.go index 8501423..e634735 100644 --- a/controllers/payinvoice.ctrl.go +++ b/controllers/payinvoice.ctrl.go @@ -7,6 +7,7 @@ import ( "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/lnd" "github.com/getsentry/sentry-go" "github.com/labstack/echo/v4" ) @@ -69,7 +70,12 @@ func (controller *PayInvoiceController) PayInvoice(c echo.Context) error { } */ - invoice, err := controller.svc.AddOutgoingInvoice(c.Request().Context(), userID, paymentRequest, decodedPaymentRequest, false) + lndPayReq := &lnd.LNDPayReq{ + PayReq: decodedPaymentRequest, + Keysend: false, + } + + invoice, err := controller.svc.AddOutgoingInvoice(c.Request().Context(), userID, paymentRequest, lndPayReq) if err != nil { return err } diff --git a/lib/service/invoices.go b/lib/service/invoices.go index f39902e..41f0572 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -10,6 +10,7 @@ import ( "github.com/getAlby/lndhub.go/common" "github.com/getAlby/lndhub.go/db/models" + "github.com/getAlby/lndhub.go/lnd" "github.com/getsentry/sentry-go" "github.com/labstack/gommon/random" "github.com/lightningnetwork/lnd/lnrpc" @@ -263,20 +264,20 @@ func (svc *LndhubService) HandleSuccessfulPayment(ctx context.Context, invoice * return err } -func (svc *LndhubService) AddOutgoingInvoice(ctx context.Context, userID int64, paymentRequest string, decodedInvoice *lnrpc.PayReq, keysend bool) (*models.Invoice, error) { +func (svc *LndhubService) AddOutgoingInvoice(ctx context.Context, userID int64, paymentRequest string, lndPayReq *lnd.LNDPayReq) (*models.Invoice, error) { // Initialize new DB invoice invoice := models.Invoice{ Type: common.InvoiceTypeOutgoing, UserID: userID, PaymentRequest: paymentRequest, - RHash: decodedInvoice.PaymentHash, - Amount: decodedInvoice.NumSatoshis, + RHash: lndPayReq.PayReq.PaymentHash, + Amount: lndPayReq.PayReq.NumSatoshis, State: common.InvoiceStateInitialized, - DestinationPubkeyHex: decodedInvoice.Destination, - DescriptionHash: decodedInvoice.DescriptionHash, - Memo: decodedInvoice.Description, - Keysend: keysend, - ExpiresAt: bun.NullTime{Time: time.Unix(decodedInvoice.Timestamp, 0).Add(time.Duration(decodedInvoice.Expiry) * time.Second)}, + DestinationPubkeyHex: lndPayReq.PayReq.Destination, + DescriptionHash: lndPayReq.PayReq.DescriptionHash, + Memo: lndPayReq.PayReq.Description, + Keysend: lndPayReq.Keysend, + ExpiresAt: bun.NullTime{Time: time.Unix(lndPayReq.PayReq.Timestamp, 0).Add(time.Duration(lndPayReq.PayReq.Expiry) * time.Second)}, } // Save invoice diff --git a/lnd/lnd.go b/lnd/lnd.go index dbfe8c8..6ee6678 100644 --- a/lnd/lnd.go +++ b/lnd/lnd.go @@ -15,6 +15,11 @@ import ( "gopkg.in/macaroon.v2" ) +type LNDPayReq struct { + PayReq *lnrpc.PayReq + Keysend bool +} + // LNDoptions are the options for the connection to the lnd node. type LNDoptions struct { Address string From fb1be3bd8ba58a245667d7d3c37133830d8a22f5 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Tue, 8 Mar 2022 13:46:51 +0100 Subject: [PATCH 14/14] Rename struct --- controllers/keysend.ctrl.go | 4 ++-- controllers/payinvoice.ctrl.go | 4 ++-- lib/service/invoices.go | 16 ++++++++-------- lnd/lnd.go | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/controllers/keysend.ctrl.go b/controllers/keysend.ctrl.go index e3fb1bc..bf28ad3 100644 --- a/controllers/keysend.ctrl.go +++ b/controllers/keysend.ctrl.go @@ -53,7 +53,7 @@ func (controller *KeySendController) KeySend(c echo.Context) error { return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) } - lndPayReq := &lnd.LNDPayReq{ + lnPayReq := &lnd.LNPayReq{ PayReq: &lnrpc.PayReq{ Destination: reqBody.Destination, NumSatoshis: reqBody.Amount, @@ -62,7 +62,7 @@ func (controller *KeySendController) KeySend(c echo.Context) error { Keysend: true, } - invoice, err := controller.svc.AddOutgoingInvoice(c.Request().Context(), userID, "", lndPayReq) + invoice, err := controller.svc.AddOutgoingInvoice(c.Request().Context(), userID, "", lnPayReq) if err != nil { return err } diff --git a/controllers/payinvoice.ctrl.go b/controllers/payinvoice.ctrl.go index e634735..1a658cd 100644 --- a/controllers/payinvoice.ctrl.go +++ b/controllers/payinvoice.ctrl.go @@ -70,12 +70,12 @@ func (controller *PayInvoiceController) PayInvoice(c echo.Context) error { } */ - lndPayReq := &lnd.LNDPayReq{ + lnPayReq := &lnd.LNPayReq{ PayReq: decodedPaymentRequest, Keysend: false, } - invoice, err := controller.svc.AddOutgoingInvoice(c.Request().Context(), userID, paymentRequest, lndPayReq) + invoice, err := controller.svc.AddOutgoingInvoice(c.Request().Context(), userID, paymentRequest, lnPayReq) if err != nil { return err } diff --git a/lib/service/invoices.go b/lib/service/invoices.go index 41f0572..087d387 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -264,20 +264,20 @@ func (svc *LndhubService) HandleSuccessfulPayment(ctx context.Context, invoice * return err } -func (svc *LndhubService) AddOutgoingInvoice(ctx context.Context, userID int64, paymentRequest string, lndPayReq *lnd.LNDPayReq) (*models.Invoice, error) { +func (svc *LndhubService) AddOutgoingInvoice(ctx context.Context, userID int64, paymentRequest string, lnPayReq *lnd.LNPayReq) (*models.Invoice, error) { // Initialize new DB invoice invoice := models.Invoice{ Type: common.InvoiceTypeOutgoing, UserID: userID, PaymentRequest: paymentRequest, - RHash: lndPayReq.PayReq.PaymentHash, - Amount: lndPayReq.PayReq.NumSatoshis, + RHash: lnPayReq.PayReq.PaymentHash, + Amount: lnPayReq.PayReq.NumSatoshis, State: common.InvoiceStateInitialized, - DestinationPubkeyHex: lndPayReq.PayReq.Destination, - DescriptionHash: lndPayReq.PayReq.DescriptionHash, - Memo: lndPayReq.PayReq.Description, - Keysend: lndPayReq.Keysend, - ExpiresAt: bun.NullTime{Time: time.Unix(lndPayReq.PayReq.Timestamp, 0).Add(time.Duration(lndPayReq.PayReq.Expiry) * time.Second)}, + DestinationPubkeyHex: lnPayReq.PayReq.Destination, + DescriptionHash: lnPayReq.PayReq.DescriptionHash, + Memo: lnPayReq.PayReq.Description, + Keysend: lnPayReq.Keysend, + ExpiresAt: bun.NullTime{Time: time.Unix(lnPayReq.PayReq.Timestamp, 0).Add(time.Duration(lnPayReq.PayReq.Expiry) * time.Second)}, } // Save invoice diff --git a/lnd/lnd.go b/lnd/lnd.go index 6ee6678..bdfe887 100644 --- a/lnd/lnd.go +++ b/lnd/lnd.go @@ -15,7 +15,7 @@ import ( "gopkg.in/macaroon.v2" ) -type LNDPayReq struct { +type LNPayReq struct { PayReq *lnrpc.PayReq Keysend bool }