From 14b1a39017d4d19d6180094bfc6109d0c74c46c5 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Tue, 28 Nov 2023 15:32:30 +0530 Subject: [PATCH 01/11] feat: add pay keysend method --- alby.go | 81 +++++++++++++++++++++++++++ handle_balance_request.go | 2 +- handle_lookup_invoice_request.go | 2 +- handle_make_invoice_request.go | 2 +- handle_pay_keysend_request.go | 96 ++++++++++++++++++++++++++++++++ handle_payment_request.go | 2 +- lnd.go | 6 ++ models.go | 20 +++++++ service.go | 8 ++- service_test.go | 5 ++ 10 files changed, 217 insertions(+), 7 deletions(-) create mode 100644 handle_pay_keysend_request.go diff --git a/alby.go b/alby.go index 255bd324..947a5783 100644 --- a/alby.go +++ b/alby.go @@ -407,6 +407,87 @@ func (svc *AlbyOAuthService) SendPaymentSync(ctx context.Context, senderPubkey, return "", errors.New(errorPayload.Message) } +func (svc *AlbyOAuthService) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, memo string, custom_records map[string]string) (preimage, paymentHash string, err error) { + app := App{} + err = svc.db.Preload("User").First(&app, &App{ + NostrPubkey: senderPubkey, + }).Error + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "payeePubkey": destination, + }).Errorf("App not found: %v", err) + return "", "", err + } + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "payeePubkey": destination, + "appId": app.ID, + "userId": app.User.ID, + }).Info("Processing payment request") + tok, err := svc.FetchUserToken(ctx, app) + if err != nil { + return "", "", err + } + client := svc.oauthConf.Client(ctx, tok) + + body := bytes.NewBuffer([]byte{}) + payload := &KeysendRequest{ + Amount: amount, + Destination: destination, + Memo: memo, + CustomRecords: custom_records, + } + err = json.NewEncoder(body).Encode(payload) + + req, err := http.NewRequest("POST", fmt.Sprintf("%s/payments/keysend", svc.cfg.AlbyAPIURL), body) + if err != nil { + svc.Logger.WithError(err).Error("Error creating request /payments/keysend") + return "", "", err + } + + req.Header.Set("User-Agent", "NWC") + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "payeePubkey": destination, + "appId": app.ID, + "userId": app.User.ID, + }).Errorf("Failed to pay keysend: %v", err) + return "", "", err + } + + if resp.StatusCode < 300 { + responsePayload := &PayResponse{} + err = json.NewDecoder(resp.Body).Decode(responsePayload) + if err != nil { + return "", "", err + } + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "payeePubkey": destination, + "appId": app.ID, + "userId": app.User.ID, + "paymentHash": responsePayload.PaymentHash, + }).Info("Payment successful") + return responsePayload.Preimage, responsePayload.PaymentHash, nil + } + + errorPayload := &ErrorResponse{} + err = json.NewDecoder(resp.Body).Decode(errorPayload) + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "payeePubkey": destination, + "appId": app.ID, + "userId": app.User.ID, + "APIHttpStatus": resp.StatusCode, + }).Errorf("Payment failed %s", string(errorPayload.Message)) + return "", "", errors.New(errorPayload.Message) +} + func (svc *AlbyOAuthService) AuthHandler(c echo.Context) error { appName := c.QueryParam("c") // c - for client // clear current session diff --git a/handle_balance_request.go b/handle_balance_request.go index 036fddbc..c39f7ebb 100644 --- a/handle_balance_request.go +++ b/handle_balance_request.go @@ -25,7 +25,7 @@ func (svc *Service) HandleGetBalanceEvent(ctx context.Context, request *Nip47Req return nil, err } - hasPermission, code, message := svc.hasPermission(&app, event, request.Method, nil) + hasPermission, code, message := svc.hasPermission(&app, event, request.Method, 0) if !hasPermission { svc.Logger.WithFields(logrus.Fields{ diff --git a/handle_lookup_invoice_request.go b/handle_lookup_invoice_request.go index 8db69f54..428fb77d 100644 --- a/handle_lookup_invoice_request.go +++ b/handle_lookup_invoice_request.go @@ -24,7 +24,7 @@ func (svc *Service) HandleLookupInvoiceEvent(ctx context.Context, request *Nip47 } // TODO: move to a shared function - hasPermission, code, message := svc.hasPermission(&app, event, request.Method, nil) + hasPermission, code, message := svc.hasPermission(&app, event, request.Method, 0) if !hasPermission { svc.Logger.WithFields(logrus.Fields{ diff --git a/handle_make_invoice_request.go b/handle_make_invoice_request.go index 7e3d0733..6a84dc5b 100644 --- a/handle_make_invoice_request.go +++ b/handle_make_invoice_request.go @@ -26,7 +26,7 @@ func (svc *Service) HandleMakeInvoiceEvent(ctx context.Context, request *Nip47Re } // TODO: move to a shared function - hasPermission, code, message := svc.hasPermission(&app, event, request.Method, nil) + hasPermission, code, message := svc.hasPermission(&app, event, request.Method, 0) if !hasPermission { svc.Logger.WithFields(logrus.Fields{ diff --git a/handle_pay_keysend_request.go b/handle_pay_keysend_request.go new file mode 100644 index 00000000..cfb76caf --- /dev/null +++ b/handle_pay_keysend_request.go @@ -0,0 +1,96 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/nbd-wtf/go-nostr" + "github.com/sirupsen/logrus" +) + +func (svc *Service) HandlePayKeysendEvent(ctx context.Context, request *Nip47Request, event *nostr.Event, app App, ss []byte) (result *nostr.Event, err error) { + + nostrEvent := NostrEvent{App: app, NostrId: event.ID, Content: event.Content, State: "received"} + err = svc.db.Create(&nostrEvent).Error + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + "appId": app.ID, + }).Errorf("Failed to save nostr event: %v", err) + return nil, err + } + + payParams := &Nip47KeysendParams{} + err = json.Unmarshal(request.Params, payParams) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + "appId": app.ID, + }).Errorf("Failed to decode nostr event: %v", err) + return nil, err + } + + hasPermission, code, message := svc.hasPermission(&app, event, request.Method, payParams.Amount) + + if !hasPermission { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + "appId": app.ID, + "senderPubkey": payParams.Pubkey, + }).Errorf("App does not have permission: %s %s", code, message) + + return svc.createResponse(event, Nip47Response{ + ResultType: request.Method, + Error: &Nip47Error{ + Code: code, + Message: message, + }}, ss) + } + + payment := Payment{App: app, NostrEvent: nostrEvent, Amount: uint(payParams.Amount / 1000)} + insertPaymentResult := svc.db.Create(&payment) + if insertPaymentResult.Error != nil { + return nil, insertPaymentResult.Error + } + + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + "appId": app.ID, + "senderPubkey": payParams.Pubkey, + }).Info("Sending payment") + + preimage, paymentHash, err := svc.lnClient.SendKeysend(ctx, event.PubKey, payParams.Amount, payParams.Pubkey, payParams.Message, payParams.TLVRecords) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + "appId": app.ID, + "senderPubkey": payParams.Pubkey, + }).Infof("Failed to send payment: %v", err) + nostrEvent.State = NOSTR_EVENT_STATE_HANDLER_ERROR + svc.db.Save(&nostrEvent) + return svc.createResponse(event, Nip47Response{ + ResultType: request.Method, + Error: &Nip47Error{ + Code: NIP_47_ERROR_INTERNAL, + Message: fmt.Sprintf("Something went wrong while paying invoice: %s", err.Error()), + }, + }, ss) + } + payment.Preimage = &preimage + nostrEvent.State = NOSTR_EVENT_STATE_HANDLER_EXECUTED + svc.db.Save(&nostrEvent) + svc.db.Save(&payment) + return svc.createResponse(event, Nip47Response{ + ResultType: request.Method, + Result: Nip47KeysendResponse{ + Preimage: preimage, + PaymentHash: paymentHash, + }, + }, ss) +} diff --git a/handle_payment_request.go b/handle_payment_request.go index 516e28c5..43851c40 100644 --- a/handle_payment_request.go +++ b/handle_payment_request.go @@ -54,7 +54,7 @@ func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, request *Nip47Req }, ss) } - hasPermission, code, message := svc.hasPermission(&app, event, request.Method, &paymentRequest) + hasPermission, code, message := svc.hasPermission(&app, event, request.Method, paymentRequest.MSatoshi) if !hasPermission { svc.Logger.WithFields(logrus.Fields{ diff --git a/lnd.go b/lnd.go index 199e34d5..8a246cd8 100644 --- a/lnd.go +++ b/lnd.go @@ -17,6 +17,7 @@ import ( type LNClient interface { SendPaymentSync(ctx context.Context, senderPubkey string, payReq string) (preimage string, err error) + SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, memo string, custom_records map[string]string) (preimage, paymentHash string, err error) GetBalance(ctx context.Context, senderPubkey string) (balance int64, err error) MakeInvoice(ctx context.Context, senderPubkey string, amount int64, description string, descriptionHash string, expiry int64) (invoice string, paymentHash string, err error) LookupInvoice(ctx context.Context, senderPubkey string, paymentHash string) (invoice string, paid bool, err error) @@ -103,6 +104,11 @@ func (svc *LNDService) SendPaymentSync(ctx context.Context, senderPubkey, payReq return hex.EncodeToString(resp.PaymentPreimage), nil } +func (svc *LNDService) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, memo string, custom_records map[string]string) (preimage, paymentHash string, err error) { + // TODO: MAKE KEYSEND PAYMENT + return "", "", errors.New("keysend not implemented") +} + func NewLNDService(ctx context.Context, svc *Service, e *echo.Echo) (result *LNDService, err error) { lndClient, err := lnd.NewLNDclient(lnd.LNDoptions{ Address: svc.cfg.LNDAddress, diff --git a/models.go b/models.go index aa5d3352..3ad85224 100644 --- a/models.go +++ b/models.go @@ -15,6 +15,7 @@ const ( NIP_47_GET_BALANCE_METHOD = "get_balance" NIP_47_MAKE_INVOICE_METHOD = "make_invoice" NIP_47_LOOKUP_INVOICE_METHOD = "lookup_invoice" + NIP_47_PAY_KEYSEND_METHOD = "pay_keysend" NIP_47_ERROR_INTERNAL = "INTERNAL" NIP_47_ERROR_NOT_IMPLEMENTED = "NOT_IMPLEMENTED" NIP_47_ERROR_QUOTA_EXCEEDED = "QUOTA_EXCEEDED" @@ -121,6 +122,13 @@ type PayRequest struct { Invoice string `json:"invoice"` } +type KeysendRequest struct { + Amount int64 `json:"amount"` + Destination string `json:"destination"` + Memo string `json:"memo,omitempty"` + CustomRecords map[string]string `json:"custom_records,omitempty"` +} + type BalanceResponse struct { Balance int64 `json:"balance"` Currency string `json:"currency"` @@ -181,6 +189,18 @@ type Nip47PayParams struct { type Nip47PayResponse struct { Preimage string `json:"preimage"` } + +type Nip47KeysendParams struct { + Amount int64 `json:"amount"` + Pubkey string `json:"pubkey"` + Message string `json:"message"` + TLVRecords map[string]string `json:"tlv_records"` +} +type Nip47KeysendResponse struct { + Preimage string `json:"preimage"` + PaymentHash string `json:"payment_hash"` +} + type Nip47BalanceResponse struct { Balance int64 `json:"balance"` MaxAmount int `json:"max_amount"` diff --git a/service.go b/service.go index 9c202b12..9d08294c 100644 --- a/service.go +++ b/service.go @@ -10,7 +10,6 @@ import ( "github.com/labstack/echo/v4" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip04" - decodepay "github.com/nbd-wtf/ln-decodepay" "github.com/sirupsen/logrus" "gorm.io/gorm" ) @@ -28,6 +27,7 @@ var supportedMethods = map[string]bool{ NIP_47_GET_BALANCE_METHOD: true, NIP_47_MAKE_INVOICE_METHOD: true, NIP_47_LOOKUP_INVOICE_METHOD: true, + NIP_47_PAY_KEYSEND_METHOD: true, } func (svc *Service) GetUser(c echo.Context) (user *User, err error) { @@ -191,6 +191,8 @@ func (svc *Service) HandleEvent(ctx context.Context, event *nostr.Event) (result switch nip47Request.Method { case NIP_47_PAY_INVOICE_METHOD: return svc.HandlePayInvoiceEvent(ctx, nip47Request, event, app, ss) + case NIP_47_PAY_KEYSEND_METHOD: + return svc.HandlePayKeysendEvent(ctx, nip47Request, event, app, ss) case NIP_47_GET_BALANCE_METHOD: return svc.HandleGetBalanceEvent(ctx, nip47Request, event, app, ss) case NIP_47_MAKE_INVOICE_METHOD: @@ -230,7 +232,7 @@ func (svc *Service) createResponse(initialEvent *nostr.Event, content interface{ return resp, nil } -func (svc *Service) hasPermission(app *App, event *nostr.Event, requestMethod string, paymentRequest *decodepay.Bolt11) (result bool, code string, message string) { +func (svc *Service) hasPermission(app *App, event *nostr.Event, requestMethod string, amount int64) (result bool, code string, message string) { // find all permissions for the app appPermissions := []AppPermission{} findPermissionsResult := svc.db.Find(&appPermissions, &AppPermission{ @@ -272,7 +274,7 @@ func (svc *Service) hasPermission(app *App, event *nostr.Event, requestMethod st if maxAmount != 0 { budgetUsage := svc.GetBudgetUsage(&appPermission) - if budgetUsage+paymentRequest.MSatoshi/1000 > int64(maxAmount) { + if budgetUsage+amount/1000 > int64(maxAmount) { return false, NIP_47_ERROR_QUOTA_EXCEEDED, "Insufficient budget remaining to make payment" } } diff --git a/service_test.go b/service_test.go index 40b819fd..14c7a83e 100644 --- a/service_test.go +++ b/service_test.go @@ -440,6 +440,11 @@ func (mln *MockLn) SendPaymentSync(ctx context.Context, senderPubkey string, pay return "123preimage", nil } +func (mln *MockLn) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, memo string, custom_records map[string]string) (preimage, paymentHash string, err error) { + //todo more advanced behaviour + return "123preimage", "123paymenthash", nil +} + func (mln *MockLn) GetBalance(ctx context.Context, senderPubkey string) (balance int64, err error) { return 21, nil } From 9b409ac206ef643a5fa805b3c9159e87f409914f Mon Sep 17 00:00:00 2001 From: im-adithya Date: Fri, 1 Dec 2023 18:37:11 +0530 Subject: [PATCH 02/11] feat: add keysend in lnd --- alby.go | 11 +++++++++-- handle_pay_keysend_request.go | 2 +- lnd.go | 32 ++++++++++++++++++++++++++++---- models.go | 9 ++++++++- service_test.go | 2 +- 5 files changed, 47 insertions(+), 9 deletions(-) diff --git a/alby.go b/alby.go index 947a5783..329112b5 100644 --- a/alby.go +++ b/alby.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "net/http" + "strconv" "github.com/labstack/echo-contrib/session" "github.com/labstack/echo/v4" @@ -407,7 +408,7 @@ func (svc *AlbyOAuthService) SendPaymentSync(ctx context.Context, senderPubkey, return "", errors.New(errorPayload.Message) } -func (svc *AlbyOAuthService) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, memo string, custom_records map[string]string) (preimage, paymentHash string, err error) { +func (svc *AlbyOAuthService) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, memo, preimage string, custom_records []TLVRecord) (preImage, paymentHash string, err error) { app := App{} err = svc.db.Preload("User").First(&app, &App{ NostrPubkey: senderPubkey, @@ -431,15 +432,21 @@ func (svc *AlbyOAuthService) SendKeysend(ctx context.Context, senderPubkey strin } client := svc.oauthConf.Client(ctx, tok) + customRecordsMap := make(map[string]string) + for _, record := range custom_records { + customRecordsMap[strconv.FormatUint(record.Type, 10)] = record.Value + } + body := bytes.NewBuffer([]byte{}) payload := &KeysendRequest{ Amount: amount, Destination: destination, Memo: memo, - CustomRecords: custom_records, + CustomRecords: customRecordsMap, } err = json.NewEncoder(body).Encode(payload) + // here we don't use the preimage from params req, err := http.NewRequest("POST", fmt.Sprintf("%s/payments/keysend", svc.cfg.AlbyAPIURL), body) if err != nil { svc.Logger.WithError(err).Error("Error creating request /payments/keysend") diff --git a/handle_pay_keysend_request.go b/handle_pay_keysend_request.go index cfb76caf..63244940 100644 --- a/handle_pay_keysend_request.go +++ b/handle_pay_keysend_request.go @@ -64,7 +64,7 @@ func (svc *Service) HandlePayKeysendEvent(ctx context.Context, request *Nip47Req "senderPubkey": payParams.Pubkey, }).Info("Sending payment") - preimage, paymentHash, err := svc.lnClient.SendKeysend(ctx, event.PubKey, payParams.Amount, payParams.Pubkey, payParams.Message, payParams.TLVRecords) + preimage, paymentHash, err := svc.lnClient.SendKeysend(ctx, event.PubKey, payParams.Amount, payParams.Pubkey, payParams.Message, payParams.Preimage, payParams.TLVRecords) if err != nil { svc.Logger.WithFields(logrus.Fields{ "eventId": event.ID, diff --git a/lnd.go b/lnd.go index 8a246cd8..c7b633c5 100644 --- a/lnd.go +++ b/lnd.go @@ -17,7 +17,7 @@ import ( type LNClient interface { SendPaymentSync(ctx context.Context, senderPubkey string, payReq string) (preimage string, err error) - SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, memo string, custom_records map[string]string) (preimage, paymentHash string, err error) + SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, memo, preimage string, custom_records []TLVRecord) (preImage, paymentHash string, err error) GetBalance(ctx context.Context, senderPubkey string) (balance int64, err error) MakeInvoice(ctx context.Context, senderPubkey string, amount int64, description string, descriptionHash string, expiry int64) (invoice string, paymentHash string, err error) LookupInvoice(ctx context.Context, senderPubkey string, paymentHash string) (invoice string, paid bool, err error) @@ -104,9 +104,33 @@ func (svc *LNDService) SendPaymentSync(ctx context.Context, senderPubkey, payReq return hex.EncodeToString(resp.PaymentPreimage), nil } -func (svc *LNDService) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, memo string, custom_records map[string]string) (preimage, paymentHash string, err error) { - // TODO: MAKE KEYSEND PAYMENT - return "", "", errors.New("keysend not implemented") +func (svc *LNDService) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, memo, preimage string, custom_records []TLVRecord) (preImage, paymentHash string, err error) { + destBytes, err := hex.DecodeString(destination) + if err != nil { + return "", "", err + } + preimageBytes, err := hex.DecodeString(preimage) + if err != nil { + return "", "", err + } + resultMap := make(map[uint64][]byte) + for _, record := range custom_records { + resultMap[record.Type] = []byte(record.Value) + } + KEYSEND_CUSTOM_RECORD := uint64(5482373484) + resultMap[KEYSEND_CUSTOM_RECORD] = preimageBytes + sendPaymentRequest := &lnrpc.SendRequest{ + Dest: destBytes, + Amt: amount, + DestFeatures: []lnrpc.FeatureBit{lnrpc.FeatureBit_TLV_ONION_REQ}, + DestCustomRecords: resultMap, + } + + resp, err := svc.client.SendPaymentSync(ctx, sendPaymentRequest) + if err != nil { + return "", "", err + } + return hex.EncodeToString(resp.PaymentPreimage), hex.EncodeToString(resp.PaymentHash), nil } func NewLNDService(ctx context.Context, svc *Service, e *echo.Echo) (result *LNDService, err error) { diff --git a/models.go b/models.go index 3ad85224..a942c72e 100644 --- a/models.go +++ b/models.go @@ -194,8 +194,15 @@ type Nip47KeysendParams struct { Amount int64 `json:"amount"` Pubkey string `json:"pubkey"` Message string `json:"message"` - TLVRecords map[string]string `json:"tlv_records"` + Preimage string `json:"preimage"` + TLVRecords []TLVRecord `json:"tlv_records"` } + +type TLVRecord struct { + Type uint64 `json:"type"` + Value string `json:"value"` +} + type Nip47KeysendResponse struct { Preimage string `json:"preimage"` PaymentHash string `json:"payment_hash"` diff --git a/service_test.go b/service_test.go index 14c7a83e..0ac1d1d5 100644 --- a/service_test.go +++ b/service_test.go @@ -440,7 +440,7 @@ func (mln *MockLn) SendPaymentSync(ctx context.Context, senderPubkey string, pay return "123preimage", nil } -func (mln *MockLn) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, memo string, custom_records map[string]string) (preimage, paymentHash string, err error) { +func (mln *MockLn) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, memo, preimage string, custom_records []TLVRecord) (preImage, paymentHash string, err error) { //todo more advanced behaviour return "123preimage", "123paymenthash", nil } From a1e872c3a2d3868b12a87c66fa8f0e2ead070205 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Mon, 11 Dec 2023 13:13:37 +0530 Subject: [PATCH 03/11] chore: update to remove memo and paymenthash --- alby.go | 17 ++++++++--------- handle_pay_keysend_request.go | 5 ++--- lnd.go | 12 ++++++------ models.go | 7 ------- service_test.go | 4 ++-- 5 files changed, 18 insertions(+), 27 deletions(-) diff --git a/alby.go b/alby.go index 329112b5..4510f4aa 100644 --- a/alby.go +++ b/alby.go @@ -408,7 +408,7 @@ func (svc *AlbyOAuthService) SendPaymentSync(ctx context.Context, senderPubkey, return "", errors.New(errorPayload.Message) } -func (svc *AlbyOAuthService) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, memo, preimage string, custom_records []TLVRecord) (preImage, paymentHash string, err error) { +func (svc *AlbyOAuthService) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, preimage string, custom_records []TLVRecord) (preImage string, err error) { app := App{} err = svc.db.Preload("User").First(&app, &App{ NostrPubkey: senderPubkey, @@ -418,7 +418,7 @@ func (svc *AlbyOAuthService) SendKeysend(ctx context.Context, senderPubkey strin "senderPubkey": senderPubkey, "payeePubkey": destination, }).Errorf("App not found: %v", err) - return "", "", err + return "", err } svc.Logger.WithFields(logrus.Fields{ "senderPubkey": senderPubkey, @@ -428,7 +428,7 @@ func (svc *AlbyOAuthService) SendKeysend(ctx context.Context, senderPubkey strin }).Info("Processing payment request") tok, err := svc.FetchUserToken(ctx, app) if err != nil { - return "", "", err + return "", err } client := svc.oauthConf.Client(ctx, tok) @@ -441,7 +441,6 @@ func (svc *AlbyOAuthService) SendKeysend(ctx context.Context, senderPubkey strin payload := &KeysendRequest{ Amount: amount, Destination: destination, - Memo: memo, CustomRecords: customRecordsMap, } err = json.NewEncoder(body).Encode(payload) @@ -450,7 +449,7 @@ func (svc *AlbyOAuthService) SendKeysend(ctx context.Context, senderPubkey strin req, err := http.NewRequest("POST", fmt.Sprintf("%s/payments/keysend", svc.cfg.AlbyAPIURL), body) if err != nil { svc.Logger.WithError(err).Error("Error creating request /payments/keysend") - return "", "", err + return "", err } req.Header.Set("User-Agent", "NWC") @@ -464,14 +463,14 @@ func (svc *AlbyOAuthService) SendKeysend(ctx context.Context, senderPubkey strin "appId": app.ID, "userId": app.User.ID, }).Errorf("Failed to pay keysend: %v", err) - return "", "", err + return "", err } if resp.StatusCode < 300 { responsePayload := &PayResponse{} err = json.NewDecoder(resp.Body).Decode(responsePayload) if err != nil { - return "", "", err + return "", err } svc.Logger.WithFields(logrus.Fields{ "senderPubkey": senderPubkey, @@ -480,7 +479,7 @@ func (svc *AlbyOAuthService) SendKeysend(ctx context.Context, senderPubkey strin "userId": app.User.ID, "paymentHash": responsePayload.PaymentHash, }).Info("Payment successful") - return responsePayload.Preimage, responsePayload.PaymentHash, nil + return responsePayload.Preimage, nil } errorPayload := &ErrorResponse{} @@ -492,7 +491,7 @@ func (svc *AlbyOAuthService) SendKeysend(ctx context.Context, senderPubkey strin "userId": app.User.ID, "APIHttpStatus": resp.StatusCode, }).Errorf("Payment failed %s", string(errorPayload.Message)) - return "", "", errors.New(errorPayload.Message) + return "", errors.New(errorPayload.Message) } func (svc *AlbyOAuthService) AuthHandler(c echo.Context) error { diff --git a/handle_pay_keysend_request.go b/handle_pay_keysend_request.go index 63244940..4cdb5b78 100644 --- a/handle_pay_keysend_request.go +++ b/handle_pay_keysend_request.go @@ -64,7 +64,7 @@ func (svc *Service) HandlePayKeysendEvent(ctx context.Context, request *Nip47Req "senderPubkey": payParams.Pubkey, }).Info("Sending payment") - preimage, paymentHash, err := svc.lnClient.SendKeysend(ctx, event.PubKey, payParams.Amount, payParams.Pubkey, payParams.Message, payParams.Preimage, payParams.TLVRecords) + preimage, err := svc.lnClient.SendKeysend(ctx, event.PubKey, payParams.Amount, payParams.Pubkey, payParams.Preimage, payParams.TLVRecords) if err != nil { svc.Logger.WithFields(logrus.Fields{ "eventId": event.ID, @@ -88,9 +88,8 @@ func (svc *Service) HandlePayKeysendEvent(ctx context.Context, request *Nip47Req svc.db.Save(&payment) return svc.createResponse(event, Nip47Response{ ResultType: request.Method, - Result: Nip47KeysendResponse{ + Result: Nip47PayResponse{ Preimage: preimage, - PaymentHash: paymentHash, }, }, ss) } diff --git a/lnd.go b/lnd.go index c7b633c5..0b498343 100644 --- a/lnd.go +++ b/lnd.go @@ -17,7 +17,7 @@ import ( type LNClient interface { SendPaymentSync(ctx context.Context, senderPubkey string, payReq string) (preimage string, err error) - SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, memo, preimage string, custom_records []TLVRecord) (preImage, paymentHash string, err error) + SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, preimage string, custom_records []TLVRecord) (preImage string, err error) GetBalance(ctx context.Context, senderPubkey string) (balance int64, err error) MakeInvoice(ctx context.Context, senderPubkey string, amount int64, description string, descriptionHash string, expiry int64) (invoice string, paymentHash string, err error) LookupInvoice(ctx context.Context, senderPubkey string, paymentHash string) (invoice string, paid bool, err error) @@ -104,14 +104,14 @@ func (svc *LNDService) SendPaymentSync(ctx context.Context, senderPubkey, payReq return hex.EncodeToString(resp.PaymentPreimage), nil } -func (svc *LNDService) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, memo, preimage string, custom_records []TLVRecord) (preImage, paymentHash string, err error) { +func (svc *LNDService) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, preimage string, custom_records []TLVRecord) (preImage string, err error) { destBytes, err := hex.DecodeString(destination) if err != nil { - return "", "", err + return "", err } preimageBytes, err := hex.DecodeString(preimage) if err != nil { - return "", "", err + return "", err } resultMap := make(map[uint64][]byte) for _, record := range custom_records { @@ -128,9 +128,9 @@ func (svc *LNDService) SendKeysend(ctx context.Context, senderPubkey string, amo resp, err := svc.client.SendPaymentSync(ctx, sendPaymentRequest) if err != nil { - return "", "", err + return "", err } - return hex.EncodeToString(resp.PaymentPreimage), hex.EncodeToString(resp.PaymentHash), nil + return hex.EncodeToString(resp.PaymentPreimage), nil } func NewLNDService(ctx context.Context, svc *Service, e *echo.Echo) (result *LNDService, err error) { diff --git a/models.go b/models.go index a942c72e..d954454d 100644 --- a/models.go +++ b/models.go @@ -125,7 +125,6 @@ type PayRequest struct { type KeysendRequest struct { Amount int64 `json:"amount"` Destination string `json:"destination"` - Memo string `json:"memo,omitempty"` CustomRecords map[string]string `json:"custom_records,omitempty"` } @@ -193,7 +192,6 @@ type Nip47PayResponse struct { type Nip47KeysendParams struct { Amount int64 `json:"amount"` Pubkey string `json:"pubkey"` - Message string `json:"message"` Preimage string `json:"preimage"` TLVRecords []TLVRecord `json:"tlv_records"` } @@ -203,11 +201,6 @@ type TLVRecord struct { Value string `json:"value"` } -type Nip47KeysendResponse struct { - Preimage string `json:"preimage"` - PaymentHash string `json:"payment_hash"` -} - type Nip47BalanceResponse struct { Balance int64 `json:"balance"` MaxAmount int `json:"max_amount"` diff --git a/service_test.go b/service_test.go index 0ac1d1d5..e44aa50c 100644 --- a/service_test.go +++ b/service_test.go @@ -440,9 +440,9 @@ func (mln *MockLn) SendPaymentSync(ctx context.Context, senderPubkey string, pay return "123preimage", nil } -func (mln *MockLn) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, memo, preimage string, custom_records []TLVRecord) (preImage, paymentHash string, err error) { +func (mln *MockLn) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, preimage string, custom_records []TLVRecord) (preImage string, err error) { //todo more advanced behaviour - return "123preimage", "123paymenthash", nil + return "123preimage", nil } func (mln *MockLn) GetBalance(ctx context.Context, senderPubkey string) (balance int64, err error) { From f6a326697d1beebf0937e3f534b2c262341d8fbe Mon Sep 17 00:00:00 2001 From: im-adithya Date: Wed, 13 Dec 2023 13:34:43 +0530 Subject: [PATCH 04/11] chore: tests and fixes --- handle_info_request.go | 2 +- handle_pay_keysend_request.go | 8 ++- service_test.go | 114 +++++++++++++++++++++++++++++----- 3 files changed, 105 insertions(+), 19 deletions(-) diff --git a/handle_info_request.go b/handle_info_request.go index 799eed18..9864bff5 100644 --- a/handle_info_request.go +++ b/handle_info_request.go @@ -21,7 +21,7 @@ func (svc *Service) HandleGetInfoEvent(ctx context.Context, request *Nip47Reques return nil, err } - hasPermission, code, message := svc.hasPermission(&app, event, request.Method, nil) + hasPermission, code, message := svc.hasPermission(&app, event, request.Method, 0) if !hasPermission { svc.Logger.WithFields(logrus.Fields{ diff --git a/handle_pay_keysend_request.go b/handle_pay_keysend_request.go index 4cdb5b78..35815749 100644 --- a/handle_pay_keysend_request.go +++ b/handle_pay_keysend_request.go @@ -33,7 +33,11 @@ func (svc *Service) HandlePayKeysendEvent(ctx context.Context, request *Nip47Req return nil, err } - hasPermission, code, message := svc.hasPermission(&app, event, request.Method, payParams.Amount) + hasPermission, code, message := svc.hasPermission(&app, event, request.Method, 0) + if hasPermission { + // We use pay_invoice permissions for budget and max amount + hasPermission, code, message = svc.hasPermission(&app, event, NIP_47_PAY_INVOICE_METHOD, payParams.Amount) + } if !hasPermission { svc.Logger.WithFields(logrus.Fields{ @@ -89,7 +93,7 @@ func (svc *Service) HandlePayKeysendEvent(ctx context.Context, request *Nip47Req return svc.createResponse(event, Nip47Response{ ResultType: request.Method, Result: Nip47PayResponse{ - Preimage: preimage, + Preimage: preimage, }, }, ss) } diff --git a/service_test.go b/service_test.go index b005822c..f147e264 100644 --- a/service_test.go +++ b/service_test.go @@ -47,27 +47,40 @@ const nip47LookupInvoiceJson = ` } } ` +const nip47KeysendJson = ` +{ + "method": "pay_keysend", + "params": { + "amount": 100, + "pubkey": "123pubkey", + "tlv_records": [{ + "type": 5482373484, + "value": "fajsn341414fq" + }] + } +} +` const nip47PayJson = ` { "method": "pay_invoice", - "params": { - "invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" + "params": { + "invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" } } ` const nip47PayWrongMethodJson = ` { "method": "get_balance", - "params": { - "invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" + "params": { + "invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" } } ` const nip47PayJsonNoInvoice = ` { "method": "pay_invoice", - "params": { - "something": "else" + "params": { + "something": "else" } } ` @@ -277,6 +290,77 @@ func TestHandleEvent(t *testing.T) { assert.NoError(t, err) assert.Equal(t, NIP_47_ERROR_RESTRICTED, received.Error.Code) assert.NotNil(t, res) + + // pay_keysend: without permission + newPayload, err = nip04.Encrypt(nip47KeysendJson, ss) + assert.NoError(t, err) + res, err = svc.HandleEvent(ctx, &nostr.Event{ + ID: "test_pay_keysend_event_1", + Kind: NIP_47_REQUEST_KIND, + PubKey: senderPubkey, + Content: newPayload, + }) + assert.NoError(t, err) + assert.NotNil(t, res) + decrypted, err = nip04.Decrypt(res.Content, ss) + assert.NoError(t, err) + received = &Nip47Response{} + err = json.Unmarshal([]byte(decrypted), received) + assert.NoError(t, err) + assert.Equal(t, NIP_47_ERROR_RESTRICTED, received.Error.Code) + assert.NotNil(t, res) + // pay_keysend: with permission + // update the existing permission to pay_invoice so we can have the budget info and increase max amount + newMaxAmount = 1000 + err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("request_method", NIP_47_PAY_INVOICE_METHOD).Update("max_amount", newMaxAmount).Error + assert.NoError(t, err) + // create a second permission for getting the budget + appPermission = &AppPermission{ + AppId: app.ID, + App: app, + RequestMethod: NIP_47_PAY_KEYSEND_METHOD, + ExpiresAt: expiresAt, + } + err = svc.db.Create(appPermission).Error + res, err = svc.HandleEvent(ctx, &nostr.Event{ + ID: "test_pay_keysend_event_2", + Kind: NIP_47_REQUEST_KIND, + PubKey: senderPubkey, + Content: newPayload, + }) + assert.NoError(t, err) + assert.NotNil(t, res) + decrypted, err = nip04.Decrypt(res.Content, ss) + assert.NoError(t, err) + received = &Nip47Response{ + Result: &Nip47PayResponse{}, + } + err = json.Unmarshal([]byte(decrypted), received) + assert.NoError(t, err) + assert.Equal(t, "123preimage", received.Result.(*Nip47PayResponse).Preimage) + + // keysend: budget overflow + newMaxAmount = 100 + // we change the budget info in pay_invoice permission + err = svc.db.Model(&AppPermission{}).Where("request_method = ?", NIP_47_PAY_INVOICE_METHOD).Update("max_amount", newMaxAmount).Error + res, err = svc.HandleEvent(ctx, &nostr.Event{ + ID: "test_pay_keysend_event_3", + Kind: NIP_47_REQUEST_KIND, + PubKey: senderPubkey, + Content: newPayload, + }) + assert.NoError(t, err) + assert.NotNil(t, res) + decrypted, err = nip04.Decrypt(res.Content, ss) + assert.NoError(t, err) + received = &Nip47Response{ + Result: &Nip47PayResponse{}, + } + err = json.Unmarshal([]byte(decrypted), received) + assert.NoError(t, err) + assert.Equal(t, NIP_47_ERROR_QUOTA_EXCEEDED, received.Error.Code) + assert.NotNil(t, res) + // get_balance: without permission newPayload, err = nip04.Encrypt(nip47GetBalanceJson, ss) assert.NoError(t, err) @@ -296,10 +380,8 @@ func TestHandleEvent(t *testing.T) { assert.Equal(t, NIP_47_ERROR_RESTRICTED, received.Error.Code) assert.NotNil(t, res) // get_balance: with permission - // update the existing permission to pay_invoice so we can get the budget info - err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("request_method", NIP_47_PAY_INVOICE_METHOD).Error - assert.NoError(t, err) - // create a second permission for getting the budget + // the pay_invoice permmission already exists with the budget info + // create a second permission to fetch the balance and budget info appPermission = &AppPermission{ AppId: app.ID, App: app, @@ -326,7 +408,7 @@ func TestHandleEvent(t *testing.T) { assert.Equal(t, 100000, received.Result.(*Nip47BalanceResponse).MaxAmount) assert.Equal(t, "never", received.Result.(*Nip47BalanceResponse).BudgetRenewal) - // make invoice: without permission + // make_invoice: without permission newPayload, err = nip04.Encrypt(nip47MakeInvoiceJson, ss) assert.NoError(t, err) res, err = svc.HandleEvent(ctx, &nostr.Event{ @@ -345,7 +427,7 @@ func TestHandleEvent(t *testing.T) { assert.Equal(t, NIP_47_ERROR_RESTRICTED, received.Error.Code) assert.NotNil(t, res) - // make invoice: with permission + // make_invoice: with permission err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("request_method", NIP_47_MAKE_INVOICE_METHOD).Error res, err = svc.HandleEvent(ctx, &nostr.Event{ ID: "test_event_13", @@ -365,7 +447,7 @@ func TestHandleEvent(t *testing.T) { assert.Equal(t, mockInvoice, received.Result.(*Nip47MakeInvoiceResponse).Invoice) assert.Equal(t, mockPaymentHash, received.Result.(*Nip47MakeInvoiceResponse).PaymentHash) - // lookup invoice: without permission + // lookup_invoice: without permission newPayload, err = nip04.Encrypt(nip47LookupInvoiceJson, ss) assert.NoError(t, err) res, err = svc.HandleEvent(ctx, &nostr.Event{ @@ -384,7 +466,7 @@ func TestHandleEvent(t *testing.T) { assert.Equal(t, NIP_47_ERROR_RESTRICTED, received.Error.Code) assert.NotNil(t, res) - // lookup invoice: with permission + // lookup_invoice: with permission err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("request_method", NIP_47_LOOKUP_INVOICE_METHOD).Error assert.NoError(t, err) res, err = svc.HandleEvent(ctx, &nostr.Event{ @@ -405,7 +487,7 @@ func TestHandleEvent(t *testing.T) { assert.Equal(t, mockInvoice, received.Result.(*Nip47LookupInvoiceResponse).Invoice) assert.Equal(t, false, received.Result.(*Nip47LookupInvoiceResponse).Paid) - // get info: without permission + // get_info: without permission newPayload, err = nip04.Encrypt(nip47GetInfoJson, ss) assert.NoError(t, err) res, err = svc.HandleEvent(ctx, &nostr.Event{ @@ -427,7 +509,7 @@ func TestHandleEvent(t *testing.T) { // delete all permissions svc.db.Exec("delete from app_permissions") - // lookup invoice: with permission + // get_info: with permission appPermission = &AppPermission{ AppId: app.ID, App: app, From 848e96e0c2310908408b0fe5f4a4f0c8eb41bcff Mon Sep 17 00:00:00 2001 From: im-adithya Date: Wed, 13 Dec 2023 13:40:39 +0530 Subject: [PATCH 05/11] chore: update README --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 628ae67b..1916457f 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,8 @@ You can also contribute to our [bounty program](https://github.com/getAlby/light ✅ `pay_invoice` +✅ `pay_keysend` + ⚠️ `make_invoice` - ⚠️ invoice in response missing (TODO) @@ -138,8 +140,6 @@ You can also contribute to our [bounty program](https://github.com/getAlby/light - ⚠️ invoice in response missing (TODO) - ⚠️ response does not match spec, missing fields -❌ `pay_keysend` - ❌ `list_transactions` ❌ `multi_pay_invoice (TBC)` @@ -159,6 +159,9 @@ You can also contribute to our [bounty program](https://github.com/getAlby/light ✅ `pay_invoice` +✅ `pay_keysend` +- ⚠️ preimage in request not supported + ⚠️ `make_invoice` - ⚠️ expiry in request not supported - ⚠️ invoice in response missing (TODO) @@ -167,8 +170,6 @@ You can also contribute to our [bounty program](https://github.com/getAlby/light - ⚠️ invoice in response missing (TODO) - ⚠️ response does not match spec, missing fields (TODO) -❌ `pay_keysend` - ❌ `list_transactions` ❌ `multi_pay_invoice (TBC)` From 0accfebddb65a76b64ad230ce7683a0b2577bcab Mon Sep 17 00:00:00 2001 From: im-adithya Date: Wed, 13 Dec 2023 15:21:17 +0530 Subject: [PATCH 06/11] chore: minor fixes --- alby.go | 10 +++++----- models.go | 3 ++- service_test.go | 2 -- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/alby.go b/alby.go index e3a4c949..21157fc3 100644 --- a/alby.go +++ b/alby.go @@ -453,7 +453,7 @@ func (svc *AlbyOAuthService) SendKeysend(ctx context.Context, senderPubkey strin "payeePubkey": destination, "appId": app.ID, "userId": app.User.ID, - }).Info("Processing payment request") + }).Info("Processing keysend request") tok, err := svc.FetchUserToken(ctx, app) if err != nil { return "", err @@ -464,11 +464,11 @@ func (svc *AlbyOAuthService) SendKeysend(ctx context.Context, senderPubkey strin for _, record := range custom_records { customRecordsMap[strconv.FormatUint(record.Type, 10)] = record.Value } - + body := bytes.NewBuffer([]byte{}) payload := &KeysendRequest{ - Amount: amount, - Destination: destination, + Amount: amount, + Destination: destination, CustomRecords: customRecordsMap, } err = json.NewEncoder(body).Encode(payload) @@ -506,7 +506,7 @@ func (svc *AlbyOAuthService) SendKeysend(ctx context.Context, senderPubkey strin "appId": app.ID, "userId": app.User.ID, "paymentHash": responsePayload.PaymentHash, - }).Info("Payment successful") + }).Info("Keysend payment successful") return responsePayload.Preimage, nil } diff --git a/models.go b/models.go index 2bb9827a..975be2c3 100644 --- a/models.go +++ b/models.go @@ -25,7 +25,7 @@ const ( NIP_47_ERROR_EXPIRED = "EXPIRED" NIP_47_ERROR_RESTRICTED = "RESTRICTED" NIP_47_OTHER = "OTHER" - NIP_47_CAPABILITIES = "pay_invoice,get_balance,get_info,make_invoice,lookup_invoice" + NIP_47_CAPABILITIES = "pay_invoice,pay_keysend,get_balance,get_info,make_invoice,lookup_invoice" ) const ( @@ -181,6 +181,7 @@ type Identity struct { Privkey string } +// TODO: move to models/Nip47 type Nip47Request struct { Method string `json:"method"` Params json.RawMessage `json:"params"` diff --git a/service_test.go b/service_test.go index f147e264..f8104482 100644 --- a/service_test.go +++ b/service_test.go @@ -572,12 +572,10 @@ type MockLn struct { } func (mln *MockLn) SendPaymentSync(ctx context.Context, senderPubkey string, payReq string) (preimage string, err error) { - //todo more advanced behaviour return "123preimage", nil } func (mln *MockLn) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, preimage string, custom_records []TLVRecord) (preImage string, err error) { - //todo more advanced behaviour return "123preimage", nil } From c64f52766a96a6a6415b0c9c547d76c8bc98b966 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Wed, 13 Dec 2023 21:17:53 +0530 Subject: [PATCH 07/11] chore: use pay_invoice permissions --- handle_pay_keysend_request.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/handle_pay_keysend_request.go b/handle_pay_keysend_request.go index 35815749..a5b8ae30 100644 --- a/handle_pay_keysend_request.go +++ b/handle_pay_keysend_request.go @@ -33,11 +33,8 @@ func (svc *Service) HandlePayKeysendEvent(ctx context.Context, request *Nip47Req return nil, err } - hasPermission, code, message := svc.hasPermission(&app, event, request.Method, 0) - if hasPermission { - // We use pay_invoice permissions for budget and max amount - hasPermission, code, message = svc.hasPermission(&app, event, NIP_47_PAY_INVOICE_METHOD, payParams.Amount) - } + // We use pay_invoice permissions for budget and max amount + hasPermission, code, message := svc.hasPermission(&app, event, NIP_47_PAY_INVOICE_METHOD, payParams.Amount) if !hasPermission { svc.Logger.WithFields(logrus.Fields{ From 27c458912930adeaac292d6a4e13cbbc917d4e28 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Wed, 13 Dec 2023 21:19:57 +0530 Subject: [PATCH 08/11] fix: remove unnecessary permission creation in test --- service_test.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/service_test.go b/service_test.go index f8104482..fd4496ee 100644 --- a/service_test.go +++ b/service_test.go @@ -314,13 +314,6 @@ func TestHandleEvent(t *testing.T) { newMaxAmount = 1000 err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("request_method", NIP_47_PAY_INVOICE_METHOD).Update("max_amount", newMaxAmount).Error assert.NoError(t, err) - // create a second permission for getting the budget - appPermission = &AppPermission{ - AppId: app.ID, - App: app, - RequestMethod: NIP_47_PAY_KEYSEND_METHOD, - ExpiresAt: expiresAt, - } err = svc.db.Create(appPermission).Error res, err = svc.HandleEvent(ctx, &nostr.Event{ ID: "test_pay_keysend_event_2", From 453cdbb6f9b3dd31c6f202274ad9fadcdb6234ac Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 14 Dec 2023 00:08:03 +0700 Subject: [PATCH 09/11] fix: keysend request permission method and convert millisats to sats --- handle_pay_keysend_request.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/handle_pay_keysend_request.go b/handle_pay_keysend_request.go index 35815749..4191fcb1 100644 --- a/handle_pay_keysend_request.go +++ b/handle_pay_keysend_request.go @@ -33,7 +33,7 @@ func (svc *Service) HandlePayKeysendEvent(ctx context.Context, request *Nip47Req return nil, err } - hasPermission, code, message := svc.hasPermission(&app, event, request.Method, 0) + hasPermission, code, message := svc.hasPermission(&app, event, NIP_47_PAY_INVOICE_METHOD, 0) if hasPermission { // We use pay_invoice permissions for budget and max amount hasPermission, code, message = svc.hasPermission(&app, event, NIP_47_PAY_INVOICE_METHOD, payParams.Amount) @@ -68,7 +68,7 @@ func (svc *Service) HandlePayKeysendEvent(ctx context.Context, request *Nip47Req "senderPubkey": payParams.Pubkey, }).Info("Sending payment") - preimage, err := svc.lnClient.SendKeysend(ctx, event.PubKey, payParams.Amount, payParams.Pubkey, payParams.Preimage, payParams.TLVRecords) + preimage, err := svc.lnClient.SendKeysend(ctx, event.PubKey, payParams.Amount/1000, payParams.Pubkey, payParams.Preimage, payParams.TLVRecords) if err != nil { svc.Logger.WithFields(logrus.Fields{ "eventId": event.ID, From b11b5b4378ca215b9b9fe61a1025ff44881f3a1e Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 14 Dec 2023 00:08:11 +0700 Subject: [PATCH 10/11] chore: also log preimage --- alby.go | 1 + 1 file changed, 1 insertion(+) diff --git a/alby.go b/alby.go index 21157fc3..3e772d95 100644 --- a/alby.go +++ b/alby.go @@ -505,6 +505,7 @@ func (svc *AlbyOAuthService) SendKeysend(ctx context.Context, senderPubkey strin "payeePubkey": destination, "appId": app.ID, "userId": app.User.ID, + "preimage": responsePayload.Preimage, "paymentHash": responsePayload.PaymentHash, }).Info("Keysend payment successful") return responsePayload.Preimage, nil From 823c1ed6bb6e40449fcdfbbdc3d1f7e43ab2ca8f Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 14 Dec 2023 00:08:25 +0700 Subject: [PATCH 11/11] fix: lnd keysend --- lnd.go | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 86 insertions(+), 9 deletions(-) diff --git a/lnd.go b/lnd.go index cec5e494..6c3a9db1 100644 --- a/lnd.go +++ b/lnd.go @@ -2,6 +2,8 @@ package main import ( "context" + "crypto/rand" + "crypto/sha256" "encoding/hex" "errors" @@ -120,33 +122,108 @@ func (svc *LNDService) SendPaymentSync(ctx context.Context, senderPubkey, payReq return hex.EncodeToString(resp.PaymentPreimage), nil } -func (svc *LNDService) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, preimage string, custom_records []TLVRecord) (preImage string, err error) { +func (svc *LNDService) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, preimage string, custom_records []TLVRecord) (respPreimage string, err error) { destBytes, err := hex.DecodeString(destination) if err != nil { return "", err } - preimageBytes, err := hex.DecodeString(preimage) - if err != nil { + var preImageBytes []byte + + if preimage == "" { + preImageBytes, err = makePreimageHex() + preimage = hex.EncodeToString(preImageBytes) + } else { + preImageBytes, err = hex.DecodeString(preimage) + } + if err != nil || len(preImageBytes) != 32 { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "destination": destination, + "preimage": preimage, + "customRecords": custom_records, + "error": err, + }).Errorf("Invalid preimage") return "", err } - resultMap := make(map[uint64][]byte) + + paymentHash := sha256.New() + paymentHash.Write(preImageBytes) + paymentHashBytes := paymentHash.Sum(nil) + paymentHashHex := hex.EncodeToString(paymentHashBytes) + + destCustomRecords := map[uint64][]byte{} for _, record := range custom_records { - resultMap[record.Type] = []byte(record.Value) + destCustomRecords[record.Type] = []byte(record.Value) } - KEYSEND_CUSTOM_RECORD := uint64(5482373484) - resultMap[KEYSEND_CUSTOM_RECORD] = preimageBytes + const KEYSEND_CUSTOM_RECORD = 5482373484 + destCustomRecords[KEYSEND_CUSTOM_RECORD] = preImageBytes sendPaymentRequest := &lnrpc.SendRequest{ Dest: destBytes, Amt: amount, + PaymentHash: paymentHashBytes, DestFeatures: []lnrpc.FeatureBit{lnrpc.FeatureBit_TLV_ONION_REQ}, - DestCustomRecords: resultMap, + DestCustomRecords: destCustomRecords, } resp, err := svc.client.SendPaymentSync(ctx, sendPaymentRequest) if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "payeePubkey": destination, + "paymentHash": paymentHashHex, + "preimage": preimage, + "customRecords": custom_records, + "error": err, + }).Errorf("Failed to send keysend payment") return "", err } - return hex.EncodeToString(resp.PaymentPreimage), nil + if resp.PaymentError != "" { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "payeePubkey": destination, + "paymentHash": paymentHashHex, + "preimage": preimage, + "customRecords": custom_records, + "paymentError": resp.PaymentError, + }).Errorf("Keysend payment has payment error") + return "", errors.New(resp.PaymentError) + } + respPreimage = hex.EncodeToString(resp.PaymentPreimage) + if respPreimage == "" { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "payeePubkey": destination, + "paymentHash": paymentHashHex, + "preimage": preimage, + "customRecords": custom_records, + "paymentError": resp.PaymentError, + }).Errorf("No preimage in keysend response") + return "", errors.New("No preimage in keysend response") + } + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "payeePubkey": destination, + "paymentHash": paymentHashHex, + "preimage": preimage, + "customRecords": custom_records, + "respPreimage": respPreimage, + }).Info("Keysend payment successful") + + return respPreimage, nil +} + +func makePreimageHex() ([]byte, error) { + bytes := make([]byte, 32) // 32 bytes * 8 bits/byte = 256 bits + _, err := rand.Read(bytes) + if err != nil { + return nil, err + } + return bytes, nil } func NewLNDService(ctx context.Context, svc *Service, e *echo.Echo) (result *LNDService, err error) {