From 5962c6bae94d84ca2ec1d4f295ed1579bfec2008 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Tue, 16 Jan 2024 19:14:24 +0530 Subject: [PATCH 01/12] feat: add multi pay invoice method --- handle_multi_pay_invoice_request.go | 158 +++++++++++++++++++++ handle_payment_request.go | 3 +- models.go | 14 ++ service.go | 206 ++++++++++++++++------------ 4 files changed, 292 insertions(+), 89 deletions(-) create mode 100644 handle_multi_pay_invoice_request.go diff --git a/handle_multi_pay_invoice_request.go b/handle_multi_pay_invoice_request.go new file mode 100644 index 00000000..237c917e --- /dev/null +++ b/handle_multi_pay_invoice_request.go @@ -0,0 +1,158 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "sync" + + "github.com/nbd-wtf/go-nostr" + decodepay "github.com/nbd-wtf/ln-decodepay" + "github.com/sirupsen/logrus" +) + +func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, request *Nip47Request, event *nostr.Event, app App, ss []byte) (results []*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 + } + + multiPayParams := &Nip47MultiPayParams{} + err = json.Unmarshal(request.Params, multiPayParams) + 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 + } + + var wg sync.WaitGroup + for _, invoiceInfo := range multiPayParams.Invoices { + wg.Add(1) + go func(invoiceInfo InvoiceInfo) { + defer wg.Done() + bolt11 := invoiceInfo.Invoice + // Convert invoice to lowercase string + bolt11 = strings.ToLower(bolt11) + paymentRequest, err := decodepay.Decodepay(bolt11) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + "appId": app.ID, + "bolt11": bolt11, + }).Errorf("Failed to decode bolt11 invoice: %v", err) + + results = svc.createAndAppendResponse(event, Nip47Response{ + ResultType: NIP_47_PAY_INVOICE_METHOD, + Error: &Nip47Error{ + Code: NIP_47_ERROR_INTERNAL, + Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()), + }, + }, ss, invoiceInfo, results) + return + } + + hasPermission, code, message := svc.hasPermission(&app, event, request.Method, paymentRequest.MSatoshi) + + if !hasPermission { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + "appId": app.ID, + }).Errorf("App does not have permission: %s %s", code, message) + + results = svc.createAndAppendResponse(event, Nip47Response{ + ResultType: NIP_47_PAY_INVOICE_METHOD, + Error: &Nip47Error{ + Code: code, + Message: message, + }, + }, ss, invoiceInfo, results) + return + } + + payment := Payment{App: app, NostrEvent: nostrEvent, PaymentRequest: bolt11, Amount: uint(paymentRequest.MSatoshi / 1000)} + insertPaymentResult := svc.db.Create(&payment) + if insertPaymentResult.Error != nil { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + "paymentRequest": bolt11, + "invoiceId": invoiceInfo.Id, + }).Errorf("Failed to process event: %v", insertPaymentResult.Error) + return + } + + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + "appId": app.ID, + "bolt11": bolt11, + }).Info("Sending payment") + + preimage, err := svc.lnClient.SendPaymentSync(ctx, event.PubKey, bolt11) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + "appId": app.ID, + "bolt11": bolt11, + }).Infof("Failed to send payment: %v", err) + // TODO: What to do here? + nostrEvent.State = NOSTR_EVENT_STATE_HANDLER_ERROR + svc.db.Save(&nostrEvent) + + results = svc.createAndAppendResponse(event, Nip47Response{ + ResultType: NIP_47_PAY_INVOICE_METHOD, + Error: &Nip47Error{ + Code: NIP_47_ERROR_INTERNAL, + Message: fmt.Sprintf("Something went wrong while paying invoice: %s", err.Error()), + }, + }, ss, invoiceInfo, results) + return + } + payment.Preimage = &preimage + // TODO: What to do here? + nostrEvent.State = NOSTR_EVENT_STATE_HANDLER_EXECUTED + svc.db.Save(&nostrEvent) + svc.db.Save(&payment) + results = svc.createAndAppendResponse(event, Nip47Response{ + ResultType: NIP_47_PAY_INVOICE_METHOD, + Result: Nip47PayResponse{ + Preimage: preimage, + }, + }, ss, invoiceInfo, results) + return + }(invoiceInfo) + } + + wg.Wait() + return results, nil +} + +func (svc *Service) createAndAppendResponse(initialEvent *nostr.Event, content interface{}, ss []byte, invoiceInfo InvoiceInfo, results []*nostr.Event) (result []*nostr.Event) { + resp, err := svc.createResponse(initialEvent, content, ss) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "eventId": initialEvent.ID, + "eventKind": initialEvent.Kind, + "paymentRequest": invoiceInfo.Invoice, + "invoiceId": invoiceInfo.Id, + }).Errorf("Failed to process event: %v", err) + return results + } + dTag := []string{"a", fmt.Sprintf("%d:%s:%d", NIP_47_RESPONSE_KIND, initialEvent.PubKey, invoiceInfo.Id)} + resp.Tags = append(resp.Tags, dTag) + return append(results, resp) +} diff --git a/handle_payment_request.go b/handle_payment_request.go index 129c7c18..0969bd5e 100644 --- a/handle_payment_request.go +++ b/handle_payment_request.go @@ -24,7 +24,6 @@ func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, request *Nip47Req return nil, err } - var bolt11 string payParams := &Nip47PayParams{} err = json.Unmarshal(request.Params, payParams) if err != nil { @@ -36,7 +35,7 @@ func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, request *Nip47Req return nil, err } - bolt11 = payParams.Invoice + bolt11 := payParams.Invoice // Convert invoice to lowercase string bolt11 = strings.ToLower(bolt11) paymentRequest, err := decodepay.Decodepay(bolt11) diff --git a/models.go b/models.go index f7ad3c42..65d73600 100644 --- a/models.go +++ b/models.go @@ -18,6 +18,7 @@ const ( NIP_47_LOOKUP_INVOICE_METHOD = "lookup_invoice" NIP_47_LIST_TRANSACTIONS_METHOD = "list_transactions" NIP_47_PAY_KEYSEND_METHOD = "pay_keysend" + NIP_47_MULTI_PAY_INVOICE_METHOD = "multi_pay_invoice" NIP_47_ERROR_INTERNAL = "INTERNAL" NIP_47_ERROR_NOT_IMPLEMENTED = "NOT_IMPLEMENTED" NIP_47_ERROR_QUOTA_EXCEEDED = "QUOTA_EXCEEDED" @@ -251,6 +252,19 @@ type Nip47PayResponse struct { Preimage string `json:"preimage"` } +type Nip47MultiPayParams struct { + Invoices []InvoiceInfo `json:"invoices"` +} + +type InvoiceInfo struct { + Id uint64 `json:"id"` + Invoice string `json:"invoice"` +} + +type Nip47MultiPayResponse struct { + Invoice string `json:"invoice"` +} + type Nip47KeysendParams struct { Amount int64 `json:"amount"` Pubkey string `json:"pubkey"` diff --git a/service.go b/service.go index 88ab3441..a339678c 100644 --- a/service.go +++ b/service.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "strings" + "sync" "time" "github.com/labstack/echo-contrib/session" @@ -55,78 +56,13 @@ func (svc *Service) GetUser(c echo.Context) (user *User, err error) { func (svc *Service) StartSubscription(ctx context.Context, sub *nostr.Subscription) error { go func() { + // block till EOS is reached <-sub.EndOfStoredEvents svc.ReceivedEOS = true svc.Logger.Info("Received EOS") - }() - go func() { for event := range sub.Events { - go func(event *nostr.Event) { - resp, err := svc.HandleEvent(ctx, event) - if err != nil { - svc.Logger.WithFields(logrus.Fields{ - "eventId": event.ID, - "eventKind": event.Kind, - }).Errorf("Failed to process event: %v", err) - } - if resp != nil { - status, err := sub.Relay.Publish(ctx, *resp) - if err != nil { - svc.Logger.WithFields(logrus.Fields{ - "eventId": event.ID, - "status": status, - "replyEventId": resp.ID, - }).Errorf("Failed to publish reply: %v", err) - return - } - - nostrEvent := NostrEvent{} - result := svc.db.Where("nostr_id = ?", event.ID).First(&nostrEvent) - if result.Error != nil { - svc.Logger.WithFields(logrus.Fields{ - "eventId": event.ID, - "status": status, - "replyEventId": resp.ID, - }).Error(result.Error) - return - } - nostrEvent.ReplyId = resp.ID - - if status == nostr.PublishStatusSucceeded { - nostrEvent.State = NOSTR_EVENT_STATE_PUBLISH_CONFIRMED - nostrEvent.RepliedAt = time.Now() - svc.db.Save(&nostrEvent) - svc.Logger.WithFields(logrus.Fields{ - "nostrEventId": nostrEvent.ID, - "eventId": event.ID, - "status": status, - "replyEventId": resp.ID, - "appId": nostrEvent.AppId, - }).Info("Published reply") - } else if status == nostr.PublishStatusFailed { - nostrEvent.State = NOSTR_EVENT_STATE_PUBLISH_FAILED - svc.db.Save(&nostrEvent) - svc.Logger.WithFields(logrus.Fields{ - "nostrEventId": nostrEvent.ID, - "eventId": event.ID, - "status": status, - "replyEventId": resp.ID, - "appId": nostrEvent.AppId, - }).Info("Failed to publish reply") - } else { - nostrEvent.State = NOSTR_EVENT_STATE_PUBLISH_UNCONFIRMED - svc.db.Save(&nostrEvent) - svc.Logger.WithFields(logrus.Fields{ - "nostrEventId": nostrEvent.ID, - "eventId": event.ID, - "status": status, - "replyEventId": resp.ID, - "appId": nostrEvent.AppId, - }).Info("Reply sent but no response from relay (timeout)") - } - } - }(event) + go svc.handleAndPublishEvent(ctx, sub, event) } svc.Logger.Info("Subscription ended") }() @@ -145,11 +81,66 @@ func (svc *Service) StartSubscription(ctx context.Context, sub *nostr.Subscripti } } -func (svc *Service) HandleEvent(ctx context.Context, event *nostr.Event) (result *nostr.Event, err error) { - //don't process historical events - if !svc.ReceivedEOS { - return nil, nil +func (svc *Service) publishEvent(ctx context.Context, sub *nostr.Subscription, event *nostr.Event, resp *nostr.Event) { + status, err := sub.Relay.Publish(ctx, *resp) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "status": status, + "replyEventId": resp.ID, + }).Errorf("Failed to publish reply: %v", err) + return + } + + nostrEvent := NostrEvent{} + result := svc.db.Where("nostr_id = ?", event.ID).First(&nostrEvent) + if result.Error != nil { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "status": status, + "replyEventId": resp.ID, + }).Error(result.Error) + return + } + nostrEvent.ReplyId = resp.ID + + if status == nostr.PublishStatusSucceeded { + nostrEvent.State = NOSTR_EVENT_STATE_PUBLISH_CONFIRMED + nostrEvent.RepliedAt = time.Now() + svc.db.Save(&nostrEvent) + svc.Logger.WithFields(logrus.Fields{ + "nostrEventId": nostrEvent.ID, + "eventId": event.ID, + "status": status, + "replyEventId": resp.ID, + "appId": nostrEvent.AppId, + }).Info("Published reply") + } else if status == nostr.PublishStatusFailed { + nostrEvent.State = NOSTR_EVENT_STATE_PUBLISH_FAILED + svc.db.Save(&nostrEvent) + svc.Logger.WithFields(logrus.Fields{ + "nostrEventId": nostrEvent.ID, + "eventId": event.ID, + "status": status, + "replyEventId": resp.ID, + "appId": nostrEvent.AppId, + }).Info("Failed to publish reply") + } else { + nostrEvent.State = NOSTR_EVENT_STATE_PUBLISH_UNCONFIRMED + svc.db.Save(&nostrEvent) + svc.Logger.WithFields(logrus.Fields{ + "nostrEventId": nostrEvent.ID, + "eventId": event.ID, + "status": status, + "replyEventId": resp.ID, + "appId": nostrEvent.AppId, + }).Info("Reply sent but no response from relay (timeout)") } +} + +func (svc *Service) handleAndPublishEvent(ctx context.Context, sub *nostr.Subscription, event *nostr.Event) { + var resp *nostr.Event + var resps []*nostr.Event svc.Logger.WithFields(logrus.Fields{ "eventId": event.ID, "eventKind": event.Kind, @@ -162,25 +153,30 @@ func (svc *Service) HandleEvent(ctx context.Context, event *nostr.Event) (result svc.Logger.WithFields(logrus.Fields{ "eventId": event.ID, }).Warn("Event already processed") - return nil, nil + return } app := App{} - err = svc.db.Preload("User").First(&app, &App{ + err := svc.db.Preload("User").First(&app, &App{ NostrPubkey: event.PubKey, }).Error if err != nil { ss, err := nip04.ComputeSharedSecret(event.PubKey, svc.cfg.NostrSecretKey) if err != nil { - return nil, err + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + }).Errorf("Failed to process event: %v", err) + return } - resp, _ := svc.createResponse(event, Nip47Response{ + resp, _ = svc.createResponse(event, Nip47Response{ Error: &Nip47Error{ Code: NIP_47_ERROR_UNAUTHORIZED, Message: "The public key does not have a wallet connected.", }, }, ss) - return resp, err + svc.publishEvent(ctx, sub, event, resp) + return } svc.Logger.WithFields(logrus.Fields{ @@ -192,7 +188,11 @@ func (svc *Service) HandleEvent(ctx context.Context, event *nostr.Event) (result //to be extra safe, decrypt using the key found from the app ss, err := nip04.ComputeSharedSecret(app.NostrPubkey, svc.cfg.NostrSecretKey) if err != nil { - return nil, err + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + }).Errorf("Failed to process event: %v", err) + return } payload, err := nip04.Decrypt(event.Content, ss) if err != nil { @@ -201,36 +201,68 @@ func (svc *Service) HandleEvent(ctx context.Context, event *nostr.Event) (result "eventKind": event.Kind, "appId": app.ID, }).Errorf("Failed to decrypt content: %v", err) - return nil, err + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + }).Errorf("Failed to process event: %v", err) + return } nip47Request := &Nip47Request{} err = json.Unmarshal([]byte(payload), nip47Request) if err != nil { - return nil, err + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + }).Errorf("Failed to process event: %v", err) + return } switch nip47Request.Method { case NIP_47_PAY_INVOICE_METHOD: - return svc.HandlePayInvoiceEvent(ctx, nip47Request, event, app, ss) + resp, err = svc.HandlePayInvoiceEvent(ctx, nip47Request, event, app, ss) case NIP_47_PAY_KEYSEND_METHOD: - return svc.HandlePayKeysendEvent(ctx, nip47Request, event, app, ss) + resp, err = svc.HandlePayKeysendEvent(ctx, nip47Request, event, app, ss) + case NIP_47_MULTI_PAY_INVOICE_METHOD: + resps, err = svc.HandleMultiPayInvoiceEvent(ctx, nip47Request, event, app, ss) case NIP_47_GET_BALANCE_METHOD: - return svc.HandleGetBalanceEvent(ctx, nip47Request, event, app, ss) + resp, err = svc.HandleGetBalanceEvent(ctx, nip47Request, event, app, ss) case NIP_47_MAKE_INVOICE_METHOD: - return svc.HandleMakeInvoiceEvent(ctx, nip47Request, event, app, ss) + resp, err = svc.HandleMakeInvoiceEvent(ctx, nip47Request, event, app, ss) case NIP_47_LOOKUP_INVOICE_METHOD: - return svc.HandleLookupInvoiceEvent(ctx, nip47Request, event, app, ss) + resp, err = svc.HandleLookupInvoiceEvent(ctx, nip47Request, event, app, ss) case NIP_47_LIST_TRANSACTIONS_METHOD: - return svc.HandleListTransactionsEvent(ctx, nip47Request, event, app, ss) + resp, err = svc.HandleListTransactionsEvent(ctx, nip47Request, event, app, ss) case NIP_47_GET_INFO_METHOD: - return svc.HandleGetInfoEvent(ctx, nip47Request, event, app, ss) + resp, err = svc.HandleGetInfoEvent(ctx, nip47Request, event, app, ss) + // multi methods default: - return svc.createResponse(event, Nip47Response{ + resp, err = svc.createResponse(event, Nip47Response{ ResultType: nip47Request.Method, Error: &Nip47Error{ Code: NIP_47_ERROR_NOT_IMPLEMENTED, Message: fmt.Sprintf("Unknown method: %s", nip47Request.Method), }}, ss) } + + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + }).Errorf("Failed to process event: %v", err) + } + if resp != nil { + svc.publishEvent(ctx, sub, event, resp) + } + if resps != nil { + var wg sync.WaitGroup + for _, resp := range resps { + wg.Add(1) + go func(resp *nostr.Event) { + defer wg.Done() + svc.publishEvent(ctx, sub, event, resp) + }(resp) + } + wg.Wait() + } } func (svc *Service) createResponse(initialEvent *nostr.Event, content interface{}, ss []byte) (result *nostr.Event, err error) { From ddbe70d79fa761c569a4cc56edc36072b7986bc2 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Wed, 17 Jan 2024 15:11:12 +0530 Subject: [PATCH 02/12] chore: publish in multi handler --- handle_multi_pay_invoice_request.go | 99 ++++++++++++++++++++--------- service.go | 24 ++----- 2 files changed, 75 insertions(+), 48 deletions(-) diff --git a/handle_multi_pay_invoice_request.go b/handle_multi_pay_invoice_request.go index 237c917e..b3a9c1c3 100644 --- a/handle_multi_pay_invoice_request.go +++ b/handle_multi_pay_invoice_request.go @@ -12,17 +12,21 @@ import ( "github.com/sirupsen/logrus" ) -func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, request *Nip47Request, event *nostr.Event, app App, ss []byte) (results []*nostr.Event, err error) { +func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, sub *nostr.Subscription, request *Nip47Request, event *nostr.Event, app App, ss []byte) { nostrEvent := NostrEvent{App: app, NostrId: event.ID, Content: event.Content, State: "received"} - err = svc.db.Create(&nostrEvent).Error + 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 + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + }).Errorf("Failed to process event: %v", err) + return } multiPayParams := &Nip47MultiPayParams{} @@ -33,7 +37,11 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, request *Nip "eventKind": event.Kind, "appId": app.ID, }).Errorf("Failed to decode nostr event: %v", err) - return nil, err + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + }).Errorf("Failed to process event: %v", err) + return } var wg sync.WaitGroup @@ -53,13 +61,25 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, request *Nip "bolt11": bolt11, }).Errorf("Failed to decode bolt11 invoice: %v", err) - results = svc.createAndAppendResponse(event, Nip47Response{ + resp, err := svc.createResponse(event, Nip47Response{ ResultType: NIP_47_PAY_INVOICE_METHOD, Error: &Nip47Error{ Code: NIP_47_ERROR_INTERNAL, Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()), }, - }, ss, invoiceInfo, results) + }, ss) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + "paymentRequest": invoiceInfo.Invoice, + "invoiceId": invoiceInfo.Id, + }).Errorf("Failed to process event: %v", err) + return + } + dTag := []string{"a", fmt.Sprintf("%d:%s:%d", NIP_47_RESPONSE_KIND, event.PubKey, invoiceInfo.Id)} + resp.Tags = append(resp.Tags, dTag) + svc.PublishEvent(ctx, sub, event, resp) return } @@ -72,13 +92,25 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, request *Nip "appId": app.ID, }).Errorf("App does not have permission: %s %s", code, message) - results = svc.createAndAppendResponse(event, Nip47Response{ + resp, err := svc.createResponse(event, Nip47Response{ ResultType: NIP_47_PAY_INVOICE_METHOD, Error: &Nip47Error{ Code: code, Message: message, }, - }, ss, invoiceInfo, results) + }, ss) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + "paymentRequest": invoiceInfo.Invoice, + "invoiceId": invoiceInfo.Id, + }).Errorf("Failed to process event: %v", err) + return + } + dTag := []string{"a", fmt.Sprintf("%d:%s:%d", NIP_47_RESPONSE_KIND, event.PubKey, invoiceInfo.Id)} + resp.Tags = append(resp.Tags, dTag) + svc.PublishEvent(ctx, sub, event, resp) return } @@ -113,13 +145,25 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, request *Nip nostrEvent.State = NOSTR_EVENT_STATE_HANDLER_ERROR svc.db.Save(&nostrEvent) - results = svc.createAndAppendResponse(event, Nip47Response{ + resp, err := svc.createResponse(event, Nip47Response{ ResultType: NIP_47_PAY_INVOICE_METHOD, Error: &Nip47Error{ Code: NIP_47_ERROR_INTERNAL, Message: fmt.Sprintf("Something went wrong while paying invoice: %s", err.Error()), }, - }, ss, invoiceInfo, results) + }, ss) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + "paymentRequest": invoiceInfo.Invoice, + "invoiceId": invoiceInfo.Id, + }).Errorf("Failed to process event: %v", err) + return + } + dTag := []string{"a", fmt.Sprintf("%d:%s:%d", NIP_47_RESPONSE_KIND, event.PubKey, invoiceInfo.Id)} + resp.Tags = append(resp.Tags, dTag) + svc.PublishEvent(ctx, sub, event, resp) return } payment.Preimage = &preimage @@ -127,32 +171,27 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, request *Nip nostrEvent.State = NOSTR_EVENT_STATE_HANDLER_EXECUTED svc.db.Save(&nostrEvent) svc.db.Save(&payment) - results = svc.createAndAppendResponse(event, Nip47Response{ + resp, err := svc.createResponse(event, Nip47Response{ ResultType: NIP_47_PAY_INVOICE_METHOD, Result: Nip47PayResponse{ Preimage: preimage, }, - }, ss, invoiceInfo, results) - return + }, ss) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + "paymentRequest": invoiceInfo.Invoice, + "invoiceId": invoiceInfo.Id, + }).Errorf("Failed to process event: %v", err) + return + } + dTag := []string{"a", fmt.Sprintf("%d:%s:%d", NIP_47_RESPONSE_KIND, event.PubKey, invoiceInfo.Id)} + resp.Tags = append(resp.Tags, dTag) + svc.PublishEvent(ctx, sub, event, resp) }(invoiceInfo) } wg.Wait() - return results, nil -} - -func (svc *Service) createAndAppendResponse(initialEvent *nostr.Event, content interface{}, ss []byte, invoiceInfo InvoiceInfo, results []*nostr.Event) (result []*nostr.Event) { - resp, err := svc.createResponse(initialEvent, content, ss) - if err != nil { - svc.Logger.WithFields(logrus.Fields{ - "eventId": initialEvent.ID, - "eventKind": initialEvent.Kind, - "paymentRequest": invoiceInfo.Invoice, - "invoiceId": invoiceInfo.Id, - }).Errorf("Failed to process event: %v", err) - return results - } - dTag := []string{"a", fmt.Sprintf("%d:%s:%d", NIP_47_RESPONSE_KIND, initialEvent.PubKey, invoiceInfo.Id)} - resp.Tags = append(resp.Tags, dTag) - return append(results, resp) + return } diff --git a/service.go b/service.go index a339678c..2d9b96e7 100644 --- a/service.go +++ b/service.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "strings" - "sync" "time" "github.com/labstack/echo-contrib/session" @@ -81,7 +80,7 @@ func (svc *Service) StartSubscription(ctx context.Context, sub *nostr.Subscripti } } -func (svc *Service) publishEvent(ctx context.Context, sub *nostr.Subscription, event *nostr.Event, resp *nostr.Event) { +func (svc *Service) PublishEvent(ctx context.Context, sub *nostr.Subscription, event *nostr.Event, resp *nostr.Event) { status, err := sub.Relay.Publish(ctx, *resp) if err != nil { svc.Logger.WithFields(logrus.Fields{ @@ -140,7 +139,6 @@ func (svc *Service) publishEvent(ctx context.Context, sub *nostr.Subscription, e func (svc *Service) handleAndPublishEvent(ctx context.Context, sub *nostr.Subscription, event *nostr.Event) { var resp *nostr.Event - var resps []*nostr.Event svc.Logger.WithFields(logrus.Fields{ "eventId": event.ID, "eventKind": event.Kind, @@ -175,7 +173,7 @@ func (svc *Service) handleAndPublishEvent(ctx context.Context, sub *nostr.Subscr Message: "The public key does not have a wallet connected.", }, }, ss) - svc.publishEvent(ctx, sub, event, resp) + svc.PublishEvent(ctx, sub, event, resp) return } @@ -217,12 +215,13 @@ func (svc *Service) handleAndPublishEvent(ctx context.Context, sub *nostr.Subscr return } switch nip47Request.Method { + case NIP_47_MULTI_PAY_INVOICE_METHOD: + svc.HandleMultiPayInvoiceEvent(ctx, sub, nip47Request, event, app, ss) + return case NIP_47_PAY_INVOICE_METHOD: resp, err = svc.HandlePayInvoiceEvent(ctx, nip47Request, event, app, ss) case NIP_47_PAY_KEYSEND_METHOD: resp, err = svc.HandlePayKeysendEvent(ctx, nip47Request, event, app, ss) - case NIP_47_MULTI_PAY_INVOICE_METHOD: - resps, err = svc.HandleMultiPayInvoiceEvent(ctx, nip47Request, event, app, ss) case NIP_47_GET_BALANCE_METHOD: resp, err = svc.HandleGetBalanceEvent(ctx, nip47Request, event, app, ss) case NIP_47_MAKE_INVOICE_METHOD: @@ -250,18 +249,7 @@ func (svc *Service) handleAndPublishEvent(ctx context.Context, sub *nostr.Subscr }).Errorf("Failed to process event: %v", err) } if resp != nil { - svc.publishEvent(ctx, sub, event, resp) - } - if resps != nil { - var wg sync.WaitGroup - for _, resp := range resps { - wg.Add(1) - go func(resp *nostr.Event) { - defer wg.Done() - svc.publishEvent(ctx, sub, event, resp) - }(resp) - } - wg.Wait() + svc.PublishEvent(ctx, sub, event, resp) } } From 09ae1fbbe8f738d47623822836607126991c1e01 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Thu, 18 Jan 2024 12:29:08 +0530 Subject: [PATCH 03/12] chore: refactoring --- handle_multi_pay_invoice_request.go | 18 +++++++++--------- models.go | 8 ++++---- service.go | 1 - 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/handle_multi_pay_invoice_request.go b/handle_multi_pay_invoice_request.go index b3a9c1c3..21c4ec2c 100644 --- a/handle_multi_pay_invoice_request.go +++ b/handle_multi_pay_invoice_request.go @@ -47,7 +47,7 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, sub *nostr.S var wg sync.WaitGroup for _, invoiceInfo := range multiPayParams.Invoices { wg.Add(1) - go func(invoiceInfo InvoiceInfo) { + go func(invoiceInfo Nip47MultiPayInvoiceElement) { defer wg.Done() bolt11 := invoiceInfo.Invoice // Convert invoice to lowercase string @@ -62,7 +62,7 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, sub *nostr.S }).Errorf("Failed to decode bolt11 invoice: %v", err) resp, err := svc.createResponse(event, Nip47Response{ - ResultType: NIP_47_PAY_INVOICE_METHOD, + ResultType: NIP_47_MULTI_PAY_INVOICE_METHOD, Error: &Nip47Error{ Code: NIP_47_ERROR_INTERNAL, Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()), @@ -77,7 +77,7 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, sub *nostr.S }).Errorf("Failed to process event: %v", err) return } - dTag := []string{"a", fmt.Sprintf("%d:%s:%d", NIP_47_RESPONSE_KIND, event.PubKey, invoiceInfo.Id)} + dTag := []string{"a", fmt.Sprintf("%d:%s:%s", NIP_47_RESPONSE_KIND, event.PubKey, invoiceInfo.Id)} resp.Tags = append(resp.Tags, dTag) svc.PublishEvent(ctx, sub, event, resp) return @@ -93,7 +93,7 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, sub *nostr.S }).Errorf("App does not have permission: %s %s", code, message) resp, err := svc.createResponse(event, Nip47Response{ - ResultType: NIP_47_PAY_INVOICE_METHOD, + ResultType: NIP_47_MULTI_PAY_INVOICE_METHOD, Error: &Nip47Error{ Code: code, Message: message, @@ -108,7 +108,7 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, sub *nostr.S }).Errorf("Failed to process event: %v", err) return } - dTag := []string{"a", fmt.Sprintf("%d:%s:%d", NIP_47_RESPONSE_KIND, event.PubKey, invoiceInfo.Id)} + dTag := []string{"a", fmt.Sprintf("%d:%s:%s", NIP_47_RESPONSE_KIND, event.PubKey, invoiceInfo.Id)} resp.Tags = append(resp.Tags, dTag) svc.PublishEvent(ctx, sub, event, resp) return @@ -146,7 +146,7 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, sub *nostr.S svc.db.Save(&nostrEvent) resp, err := svc.createResponse(event, Nip47Response{ - ResultType: NIP_47_PAY_INVOICE_METHOD, + ResultType: NIP_47_MULTI_PAY_INVOICE_METHOD, Error: &Nip47Error{ Code: NIP_47_ERROR_INTERNAL, Message: fmt.Sprintf("Something went wrong while paying invoice: %s", err.Error()), @@ -161,7 +161,7 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, sub *nostr.S }).Errorf("Failed to process event: %v", err) return } - dTag := []string{"a", fmt.Sprintf("%d:%s:%d", NIP_47_RESPONSE_KIND, event.PubKey, invoiceInfo.Id)} + dTag := []string{"a", fmt.Sprintf("%d:%s:%s", NIP_47_RESPONSE_KIND, event.PubKey, invoiceInfo.Id)} resp.Tags = append(resp.Tags, dTag) svc.PublishEvent(ctx, sub, event, resp) return @@ -172,7 +172,7 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, sub *nostr.S svc.db.Save(&nostrEvent) svc.db.Save(&payment) resp, err := svc.createResponse(event, Nip47Response{ - ResultType: NIP_47_PAY_INVOICE_METHOD, + ResultType: NIP_47_MULTI_PAY_INVOICE_METHOD, Result: Nip47PayResponse{ Preimage: preimage, }, @@ -186,7 +186,7 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, sub *nostr.S }).Errorf("Failed to process event: %v", err) return } - dTag := []string{"a", fmt.Sprintf("%d:%s:%d", NIP_47_RESPONSE_KIND, event.PubKey, invoiceInfo.Id)} + dTag := []string{"a", fmt.Sprintf("%d:%s:%s", NIP_47_RESPONSE_KIND, event.PubKey, invoiceInfo.Id)} resp.Tags = append(resp.Tags, dTag) svc.PublishEvent(ctx, sub, event, resp) }(invoiceInfo) diff --git a/models.go b/models.go index 65d73600..12b014a4 100644 --- a/models.go +++ b/models.go @@ -253,12 +253,12 @@ type Nip47PayResponse struct { } type Nip47MultiPayParams struct { - Invoices []InvoiceInfo `json:"invoices"` + Invoices []Nip47MultiPayInvoiceElement `json:"invoices"` } -type InvoiceInfo struct { - Id uint64 `json:"id"` - Invoice string `json:"invoice"` +type Nip47MultiPayInvoiceElement struct { + Nip47PayParams + Id string `json:"id"` } type Nip47MultiPayResponse struct { diff --git a/service.go b/service.go index 2d9b96e7..7dc2ed89 100644 --- a/service.go +++ b/service.go @@ -232,7 +232,6 @@ func (svc *Service) handleAndPublishEvent(ctx context.Context, sub *nostr.Subscr resp, err = svc.HandleListTransactionsEvent(ctx, nip47Request, event, app, ss) case NIP_47_GET_INFO_METHOD: resp, err = svc.HandleGetInfoEvent(ctx, nip47Request, event, app, ss) - // multi methods default: resp, err = svc.createResponse(event, Nip47Response{ ResultType: nip47Request.Method, From b1a43fe38b01da7664d311c115e99aeced35b0ca Mon Sep 17 00:00:00 2001 From: im-adithya Date: Thu, 18 Jan 2024 15:40:01 +0530 Subject: [PATCH 04/12] chore: DRY up dtag usage --- handle_multi_pay_invoice_request.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/handle_multi_pay_invoice_request.go b/handle_multi_pay_invoice_request.go index 21c4ec2c..4a865415 100644 --- a/handle_multi_pay_invoice_request.go +++ b/handle_multi_pay_invoice_request.go @@ -77,12 +77,19 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, sub *nostr.S }).Errorf("Failed to process event: %v", err) return } + // TODO: Decide what to do if id is empty dTag := []string{"a", fmt.Sprintf("%d:%s:%s", NIP_47_RESPONSE_KIND, event.PubKey, invoiceInfo.Id)} resp.Tags = append(resp.Tags, dTag) svc.PublishEvent(ctx, sub, event, resp) return } + id := invoiceInfo.Id + if id == "" { + id = paymentRequest.PaymentHash + } + dTag := []string{"a", fmt.Sprintf("%d:%s:%s", NIP_47_RESPONSE_KIND, event.PubKey, id)} + hasPermission, code, message := svc.hasPermission(&app, event, request.Method, paymentRequest.MSatoshi) if !hasPermission { @@ -108,7 +115,6 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, sub *nostr.S }).Errorf("Failed to process event: %v", err) return } - dTag := []string{"a", fmt.Sprintf("%d:%s:%s", NIP_47_RESPONSE_KIND, event.PubKey, invoiceInfo.Id)} resp.Tags = append(resp.Tags, dTag) svc.PublishEvent(ctx, sub, event, resp) return @@ -161,7 +167,6 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, sub *nostr.S }).Errorf("Failed to process event: %v", err) return } - dTag := []string{"a", fmt.Sprintf("%d:%s:%s", NIP_47_RESPONSE_KIND, event.PubKey, invoiceInfo.Id)} resp.Tags = append(resp.Tags, dTag) svc.PublishEvent(ctx, sub, event, resp) return @@ -186,7 +191,6 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, sub *nostr.S }).Errorf("Failed to process event: %v", err) return } - dTag := []string{"a", fmt.Sprintf("%d:%s:%s", NIP_47_RESPONSE_KIND, event.PubKey, invoiceInfo.Id)} resp.Tags = append(resp.Tags, dTag) svc.PublishEvent(ctx, sub, event, resp) }(invoiceInfo) From 3fad68889864e241ad7538c2b68c3f57887e4c98 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 22 Jan 2024 18:49:38 +0700 Subject: [PATCH 05/12] fix: issues with multi_pay_invoice - fix permission check - do not change tags after decrypting --- README.md | 6 ++++++ handle_balance_request.go | 7 +++---- handle_info_request.go | 6 +++--- handle_list_transactions_request.go | 7 +++---- handle_lookup_invoice_request.go | 9 ++++----- handle_make_invoice_request.go | 9 ++++----- handle_multi_pay_invoice_request.go | 27 ++++++++++++--------------- handle_pay_keysend_request.go | 6 +++--- handle_payment_request.go | 8 ++++---- models.go | 2 +- service.go | 12 ++++++++---- 11 files changed, 51 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 6e3faf54..283e33d4 100644 --- a/README.md +++ b/README.md @@ -133,12 +133,15 @@ You can also contribute to our [bounty program](https://github.com/getAlby/light ✅ `pay_invoice` - ⚠️ amount not supported (for amountless invoices) +- ⚠️ PAYMENT_FAILED error code not supported ✅ `pay_keysend` +- ⚠️ PAYMENT_FAILED error code not supported ✅ `make_invoice` ✅ `lookup_invoice` +- ⚠️ NOT_FOUND error code not supported ✅ `list_transactions` - ⚠️ from and until in request not supported @@ -161,15 +164,18 @@ You can also contribute to our [bounty program](https://github.com/getAlby/light ✅ `pay_invoice` - ⚠️ amount not supported (for amountless invoices) +- ⚠️ PAYMENT_FAILED error code not supported ✅ `pay_keysend` - ⚠️ preimage in request not supported +- ⚠️ PAYMENT_FAILED error code not supported ✅ `make_invoice` - ⚠️ expiry in request not supported ✅ `lookup_invoice` - ⚠️ fees_paid in response not supported +- ⚠️ NOT_FOUND error code not supported ✅ `list_transactions` - ⚠️ offset and unpaid in request not supported diff --git a/handle_balance_request.go b/handle_balance_request.go index c39f7ebb..2e68e22b 100644 --- a/handle_balance_request.go +++ b/handle_balance_request.go @@ -39,7 +39,7 @@ func (svc *Service) HandleGetBalanceEvent(ctx context.Context, request *Nip47Req Error: &Nip47Error{ Code: code, Message: message, - }}, ss) + }}, nostr.Tags{}, ss) } svc.Logger.WithFields(logrus.Fields{ @@ -63,7 +63,7 @@ func (svc *Service) HandleGetBalanceEvent(ctx context.Context, request *Nip47Req Code: NIP_47_ERROR_INTERNAL, Message: fmt.Sprintf("Something went wrong while fetching balance: %s", err.Error()), }, - }, ss) + }, nostr.Tags{}, ss) } responsePayload := &Nip47BalanceResponse{ @@ -84,6 +84,5 @@ func (svc *Service) HandleGetBalanceEvent(ctx context.Context, request *Nip47Req return svc.createResponse(event, Nip47Response{ ResultType: NIP_47_GET_BALANCE_METHOD, Result: responsePayload, - }, - ss) + }, nostr.Tags{}, ss) } diff --git a/handle_info_request.go b/handle_info_request.go index 9864bff5..041be66b 100644 --- a/handle_info_request.go +++ b/handle_info_request.go @@ -35,7 +35,7 @@ func (svc *Service) HandleGetInfoEvent(ctx context.Context, request *Nip47Reques Error: &Nip47Error{ Code: code, Message: message, - }}, ss) + }}, nostr.Tags{}, ss) } svc.Logger.WithFields(logrus.Fields{ @@ -59,7 +59,7 @@ func (svc *Service) HandleGetInfoEvent(ctx context.Context, request *Nip47Reques Code: NIP_47_ERROR_INTERNAL, Message: fmt.Sprintf("Something went wrong while fetching node info: %s", err.Error()), }, - }, ss) + }, nostr.Tags{}, ss) } responsePayload := &Nip47GetInfoResponse{ @@ -77,5 +77,5 @@ func (svc *Service) HandleGetInfoEvent(ctx context.Context, request *Nip47Reques return svc.createResponse(event, Nip47Response{ ResultType: request.Method, Result: responsePayload, - }, ss) + }, nostr.Tags{}, ss) } diff --git a/handle_list_transactions_request.go b/handle_list_transactions_request.go index 46bd3a1c..acc9ec72 100644 --- a/handle_list_transactions_request.go +++ b/handle_list_transactions_request.go @@ -48,7 +48,7 @@ func (svc *Service) HandleListTransactionsEvent(ctx context.Context, request *Ni Error: &Nip47Error{ Code: code, Message: message, - }}, ss) + }}, nostr.Tags{}, ss) } svc.Logger.WithFields(logrus.Fields{ @@ -74,7 +74,7 @@ func (svc *Service) HandleListTransactionsEvent(ctx context.Context, request *Ni Code: NIP_47_ERROR_INTERNAL, Message: fmt.Sprintf("Something went wrong while fetching transactions: %s", err.Error()), }, - }, ss) + }, nostr.Tags{}, ss) } responsePayload := &Nip47ListTransactionsResponse{ @@ -87,6 +87,5 @@ func (svc *Service) HandleListTransactionsEvent(ctx context.Context, request *Ni return svc.createResponse(event, Nip47Response{ ResultType: request.Method, Result: responsePayload, - }, - ss) + }, nostr.Tags{}, ss) } diff --git a/handle_lookup_invoice_request.go b/handle_lookup_invoice_request.go index 358cc991..de1d979d 100644 --- a/handle_lookup_invoice_request.go +++ b/handle_lookup_invoice_request.go @@ -39,7 +39,7 @@ func (svc *Service) HandleLookupInvoiceEvent(ctx context.Context, request *Nip47 Error: &Nip47Error{ Code: code, Message: message, - }}, ss) + }}, nostr.Tags{}, ss) } // TODO: move to a shared generic function @@ -80,7 +80,7 @@ func (svc *Service) HandleLookupInvoiceEvent(ctx context.Context, request *Nip47 Code: NIP_47_ERROR_INTERNAL, Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()), }, - }, ss) + }, nostr.Tags{}, ss) } paymentHash = paymentRequest.PaymentHash } @@ -102,7 +102,7 @@ func (svc *Service) HandleLookupInvoiceEvent(ctx context.Context, request *Nip47 Code: NIP_47_ERROR_INTERNAL, Message: fmt.Sprintf("Something went wrong while looking up invoice: %s", err.Error()), }, - }, ss) + }, nostr.Tags{}, ss) } responsePayload := &Nip47LookupInvoiceResponse{ @@ -114,6 +114,5 @@ func (svc *Service) HandleLookupInvoiceEvent(ctx context.Context, request *Nip47 return svc.createResponse(event, Nip47Response{ ResultType: NIP_47_LOOKUP_INVOICE_METHOD, Result: responsePayload, - }, - ss) + }, nostr.Tags{}, ss) } diff --git a/handle_make_invoice_request.go b/handle_make_invoice_request.go index 5269c694..952aeda7 100644 --- a/handle_make_invoice_request.go +++ b/handle_make_invoice_request.go @@ -40,7 +40,7 @@ func (svc *Service) HandleMakeInvoiceEvent(ctx context.Context, request *Nip47Re Error: &Nip47Error{ Code: code, Message: message, - }}, ss) + }}, nostr.Tags{}, ss) } // TODO: move to a shared generic function @@ -68,7 +68,7 @@ func (svc *Service) HandleMakeInvoiceEvent(ctx context.Context, request *Nip47Re Code: NIP_47_OTHER, Message: "Only one of description, description_hash can be provided", }, - }, ss) + }, nostr.Tags{}, ss) } svc.Logger.WithFields(logrus.Fields{ @@ -100,7 +100,7 @@ func (svc *Service) HandleMakeInvoiceEvent(ctx context.Context, request *Nip47Re Code: NIP_47_ERROR_INTERNAL, Message: fmt.Sprintf("Something went wrong while making invoice: %s", err.Error()), }, - }, ss) + }, nostr.Tags{}, ss) } responsePayload := &Nip47MakeInvoiceResponse{ @@ -112,6 +112,5 @@ func (svc *Service) HandleMakeInvoiceEvent(ctx context.Context, request *Nip47Re return svc.createResponse(event, Nip47Response{ ResultType: NIP_47_MAKE_INVOICE_METHOD, Result: responsePayload, - }, - ss) + }, nostr.Tags{}, ss) } diff --git a/handle_multi_pay_invoice_request.go b/handle_multi_pay_invoice_request.go index 4a865415..865af37a 100644 --- a/handle_multi_pay_invoice_request.go +++ b/handle_multi_pay_invoice_request.go @@ -61,13 +61,15 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, sub *nostr.S "bolt11": bolt11, }).Errorf("Failed to decode bolt11 invoice: %v", err) + // TODO: Decide what to do if id is empty + dTag := []string{"d", invoiceInfo.Id} resp, err := svc.createResponse(event, Nip47Response{ ResultType: NIP_47_MULTI_PAY_INVOICE_METHOD, Error: &Nip47Error{ Code: NIP_47_ERROR_INTERNAL, Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()), }, - }, ss) + }, nostr.Tags{dTag}, ss) if err != nil { svc.Logger.WithFields(logrus.Fields{ "eventId": event.ID, @@ -77,20 +79,18 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, sub *nostr.S }).Errorf("Failed to process event: %v", err) return } - // TODO: Decide what to do if id is empty - dTag := []string{"a", fmt.Sprintf("%d:%s:%s", NIP_47_RESPONSE_KIND, event.PubKey, invoiceInfo.Id)} - resp.Tags = append(resp.Tags, dTag) + svc.PublishEvent(ctx, sub, event, resp) return } - id := invoiceInfo.Id - if id == "" { - id = paymentRequest.PaymentHash + invoiceDTagValue := invoiceInfo.Id + if invoiceDTagValue == "" { + invoiceDTagValue = paymentRequest.PaymentHash } - dTag := []string{"a", fmt.Sprintf("%d:%s:%s", NIP_47_RESPONSE_KIND, event.PubKey, id)} + dTag := []string{"d", invoiceDTagValue} - hasPermission, code, message := svc.hasPermission(&app, event, request.Method, paymentRequest.MSatoshi) + hasPermission, code, message := svc.hasPermission(&app, event, NIP_47_PAY_INVOICE_METHOD, paymentRequest.MSatoshi) if !hasPermission { svc.Logger.WithFields(logrus.Fields{ @@ -105,7 +105,7 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, sub *nostr.S Code: code, Message: message, }, - }, ss) + }, nostr.Tags{dTag}, ss) if err != nil { svc.Logger.WithFields(logrus.Fields{ "eventId": event.ID, @@ -115,7 +115,6 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, sub *nostr.S }).Errorf("Failed to process event: %v", err) return } - resp.Tags = append(resp.Tags, dTag) svc.PublishEvent(ctx, sub, event, resp) return } @@ -157,7 +156,7 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, sub *nostr.S Code: NIP_47_ERROR_INTERNAL, Message: fmt.Sprintf("Something went wrong while paying invoice: %s", err.Error()), }, - }, ss) + }, nostr.Tags{dTag}, ss) if err != nil { svc.Logger.WithFields(logrus.Fields{ "eventId": event.ID, @@ -167,7 +166,6 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, sub *nostr.S }).Errorf("Failed to process event: %v", err) return } - resp.Tags = append(resp.Tags, dTag) svc.PublishEvent(ctx, sub, event, resp) return } @@ -181,7 +179,7 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, sub *nostr.S Result: Nip47PayResponse{ Preimage: preimage, }, - }, ss) + }, nostr.Tags{dTag}, ss) if err != nil { svc.Logger.WithFields(logrus.Fields{ "eventId": event.ID, @@ -191,7 +189,6 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, sub *nostr.S }).Errorf("Failed to process event: %v", err) return } - resp.Tags = append(resp.Tags, dTag) svc.PublishEvent(ctx, sub, event, resp) }(invoiceInfo) } diff --git a/handle_pay_keysend_request.go b/handle_pay_keysend_request.go index e853f32c..7ebf7493 100644 --- a/handle_pay_keysend_request.go +++ b/handle_pay_keysend_request.go @@ -49,7 +49,7 @@ func (svc *Service) HandlePayKeysendEvent(ctx context.Context, request *Nip47Req Error: &Nip47Error{ Code: code, Message: message, - }}, ss) + }}, nostr.Tags{}, ss) } payment := Payment{App: app, NostrEvent: nostrEvent, Amount: uint(payParams.Amount / 1000)} @@ -81,7 +81,7 @@ func (svc *Service) HandlePayKeysendEvent(ctx context.Context, request *Nip47Req Code: NIP_47_ERROR_INTERNAL, Message: fmt.Sprintf("Something went wrong while paying invoice: %s", err.Error()), }, - }, ss) + }, nostr.Tags{}, ss) } payment.Preimage = &preimage nostrEvent.State = NOSTR_EVENT_STATE_HANDLER_EXECUTED @@ -92,5 +92,5 @@ func (svc *Service) HandlePayKeysendEvent(ctx context.Context, request *Nip47Req Result: Nip47PayResponse{ Preimage: preimage, }, - }, ss) + }, nostr.Tags{}, ss) } diff --git a/handle_payment_request.go b/handle_payment_request.go index 0969bd5e..12550d2f 100644 --- a/handle_payment_request.go +++ b/handle_payment_request.go @@ -53,7 +53,7 @@ func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, request *Nip47Req Code: NIP_47_ERROR_INTERNAL, Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()), }, - }, ss) + }, nostr.Tags{}, ss) } hasPermission, code, message := svc.hasPermission(&app, event, request.Method, paymentRequest.MSatoshi) @@ -70,7 +70,7 @@ func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, request *Nip47Req Error: &Nip47Error{ Code: code, Message: message, - }}, ss) + }}, nostr.Tags{}, ss) } payment := Payment{App: app, NostrEvent: nostrEvent, PaymentRequest: bolt11, Amount: uint(paymentRequest.MSatoshi / 1000)} @@ -102,7 +102,7 @@ func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, request *Nip47Req Code: NIP_47_ERROR_INTERNAL, Message: fmt.Sprintf("Something went wrong while paying invoice: %s", err.Error()), }, - }, ss) + }, nostr.Tags{}, ss) } payment.Preimage = &preimage nostrEvent.State = NOSTR_EVENT_STATE_HANDLER_EXECUTED @@ -113,5 +113,5 @@ func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, request *Nip47Req Result: Nip47PayResponse{ Preimage: preimage, }, - }, ss) + }, nostr.Tags{}, ss) } diff --git a/models.go b/models.go index 12b014a4..11e82165 100644 --- a/models.go +++ b/models.go @@ -27,7 +27,7 @@ const ( NIP_47_ERROR_EXPIRED = "EXPIRED" NIP_47_ERROR_RESTRICTED = "RESTRICTED" NIP_47_OTHER = "OTHER" - NIP_47_CAPABILITIES = "pay_invoice,pay_keysend,get_balance,get_info,make_invoice,lookup_invoice,list_transactions" + NIP_47_CAPABILITIES = "pay_invoice,pay_keysend,get_balance,get_info,make_invoice,lookup_invoice,list_transactions,multi_pay_invoice" ) const ( diff --git a/service.go b/service.go index 7dc2ed89..03a6f2d3 100644 --- a/service.go +++ b/service.go @@ -172,7 +172,7 @@ func (svc *Service) handleAndPublishEvent(ctx context.Context, sub *nostr.Subscr Code: NIP_47_ERROR_UNAUTHORIZED, Message: "The public key does not have a wallet connected.", }, - }, ss) + }, nostr.Tags{}, ss) svc.PublishEvent(ctx, sub, event, resp) return } @@ -238,7 +238,7 @@ func (svc *Service) handleAndPublishEvent(ctx context.Context, sub *nostr.Subscr Error: &Nip47Error{ Code: NIP_47_ERROR_NOT_IMPLEMENTED, Message: fmt.Sprintf("Unknown method: %s", nip47Request.Method), - }}, ss) + }}, nostr.Tags{}, ss) } if err != nil { @@ -252,7 +252,7 @@ func (svc *Service) handleAndPublishEvent(ctx context.Context, sub *nostr.Subscr } } -func (svc *Service) createResponse(initialEvent *nostr.Event, content interface{}, ss []byte) (result *nostr.Event, err error) { +func (svc *Service) createResponse(initialEvent *nostr.Event, content interface{}, tags nostr.Tags, ss []byte) (result *nostr.Event, err error) { payloadBytes, err := json.Marshal(content) if err != nil { return nil, err @@ -261,11 +261,15 @@ func (svc *Service) createResponse(initialEvent *nostr.Event, content interface{ if err != nil { return nil, err } + + allTags := nostr.Tags{[]string{"p", initialEvent.PubKey}, []string{"e", initialEvent.ID}} + allTags = append(allTags, tags...) + resp := &nostr.Event{ PubKey: svc.cfg.IdentityPubkey, CreatedAt: nostr.Now(), Kind: NIP_47_RESPONSE_KIND, - Tags: nostr.Tags{[]string{"p", initialEvent.PubKey}, []string{"e", initialEvent.ID}}, + Tags: allTags, Content: msg, } err = resp.Sign(svc.cfg.NostrSecretKey) From dad7b9067197e8e4ccebbef342ecdb7e1e4b92df Mon Sep 17 00:00:00 2001 From: im-adithya Date: Mon, 22 Jan 2024 19:20:27 +0530 Subject: [PATCH 06/12] chore: use sync mutex for concurrency support --- alby.go | 89 +++++++++++++++++++++++++++++++++------------------------ 1 file changed, 51 insertions(+), 38 deletions(-) diff --git a/alby.go b/alby.go index 65205486..f50c1bc5 100644 --- a/alby.go +++ b/alby.go @@ -9,6 +9,8 @@ import ( "net/http" "net/url" "strconv" + "sync" + "time" "github.com/labstack/echo-contrib/session" "github.com/labstack/echo/v4" @@ -18,10 +20,11 @@ import ( ) type AlbyOAuthService struct { - cfg *Config - oauthConf *oauth2.Config - db *gorm.DB - Logger *logrus.Logger + cfg *Config + oauthConf *oauth2.Config + db *gorm.DB + Logger *logrus.Logger + tokenMutex sync.Mutex } func NewAlbyOauthService(svc *Service, e *echo.Echo) (result *AlbyOAuthService, err error) { @@ -52,12 +55,22 @@ func NewAlbyOauthService(svc *Service, e *echo.Echo) (result *AlbyOAuthService, } func (svc *AlbyOAuthService) FetchUserToken(ctx context.Context, app App) (token *oauth2.Token, err error) { - user := app.User - tok, err := svc.oauthConf.TokenSource(ctx, &oauth2.Token{ + svc.tokenMutex.Lock() + defer svc.tokenMutex.Unlock() + + user := User{} + err = svc.db.First(&user, &User{ID: app.UserId}).Error + currentToken := &oauth2.Token{ AccessToken: user.AccessToken, RefreshToken: user.RefreshToken, Expiry: user.Expiry, - }).Token() + } + + if user.Expiry.After(time.Now()) { + return currentToken, nil + } + + tok, err := svc.oauthConf.TokenSource(ctx, currentToken).Token() if err != nil { svc.Logger.WithFields(logrus.Fields{ "senderPubkey": app.NostrPubkey, @@ -82,7 +95,7 @@ func (svc *AlbyOAuthService) FetchUserToken(ctx context.Context, app App) (token func (svc *AlbyOAuthService) MakeInvoice(ctx context.Context, senderPubkey string, amount int64, description string, descriptionHash string, expiry int64) (transaction *Nip47Transaction, err error) { // TODO: move to a shared function app := App{} - err = svc.db.Preload("User").First(&app, &App{ + err = svc.db.First(&app, &App{ NostrPubkey: senderPubkey, }).Error if err != nil { @@ -117,7 +130,7 @@ func (svc *AlbyOAuthService) MakeInvoice(ctx context.Context, senderPubkey strin "descriptionHash": descriptionHash, "expiry": expiry, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, }).Info("Processing make invoice request") tok, err := svc.FetchUserToken(ctx, app) if err != nil { @@ -154,7 +167,7 @@ func (svc *AlbyOAuthService) MakeInvoice(ctx context.Context, senderPubkey strin "descriptionHash": descriptionHash, "expiry": expiry, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, }).Errorf("Failed to make invoice: %v", err) return nil, err } @@ -172,7 +185,7 @@ func (svc *AlbyOAuthService) MakeInvoice(ctx context.Context, senderPubkey strin "descriptionHash": descriptionHash, "expiry": expiry, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, "paymentRequest": responsePayload.PaymentRequest, "paymentHash": responsePayload.PaymentHash, }).Info("Make invoice successful") @@ -190,7 +203,7 @@ func (svc *AlbyOAuthService) MakeInvoice(ctx context.Context, senderPubkey strin "descriptionHash": descriptionHash, "expiry": expiry, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, "APIHttpStatus": resp.StatusCode, }).Errorf("Make invoice failed %s", string(errorPayload.Message)) return nil, errors.New(errorPayload.Message) @@ -199,7 +212,7 @@ func (svc *AlbyOAuthService) MakeInvoice(ctx context.Context, senderPubkey strin func (svc *AlbyOAuthService) LookupInvoice(ctx context.Context, senderPubkey string, paymentHash string) (transaction *Nip47Transaction, err error) { // TODO: move to a shared function app := App{} - err = svc.db.Preload("User").First(&app, &App{ + err = svc.db.First(&app, &App{ NostrPubkey: senderPubkey, }).Error if err != nil { @@ -214,7 +227,7 @@ func (svc *AlbyOAuthService) LookupInvoice(ctx context.Context, senderPubkey str "senderPubkey": senderPubkey, "paymentHash": paymentHash, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, }).Info("Processing lookup invoice request") tok, err := svc.FetchUserToken(ctx, app) if err != nil { @@ -240,7 +253,7 @@ func (svc *AlbyOAuthService) LookupInvoice(ctx context.Context, senderPubkey str "senderPubkey": senderPubkey, "paymentHash": paymentHash, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, }).Errorf("Failed to lookup invoice: %v", err) return nil, err } @@ -255,7 +268,7 @@ func (svc *AlbyOAuthService) LookupInvoice(ctx context.Context, senderPubkey str "senderPubkey": senderPubkey, "paymentHash": paymentHash, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, "paymentRequest": responsePayload.PaymentRequest, "settled": responsePayload.Settled, }).Info("Lookup invoice successful") @@ -270,7 +283,7 @@ func (svc *AlbyOAuthService) LookupInvoice(ctx context.Context, senderPubkey str "senderPubkey": senderPubkey, "paymentHash": paymentHash, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, "APIHttpStatus": resp.StatusCode, }).Errorf("Lookup invoice failed %s", string(errorPayload.Message)) return nil, errors.New(errorPayload.Message) @@ -278,7 +291,7 @@ func (svc *AlbyOAuthService) LookupInvoice(ctx context.Context, senderPubkey str func (svc *AlbyOAuthService) GetInfo(ctx context.Context, senderPubkey string) (info *NodeInfo, err error) { app := App{} - err = svc.db.Preload("User").First(&app, &App{ + err = svc.db.First(&app, &App{ NostrPubkey: senderPubkey, }).Error if err != nil { @@ -291,7 +304,7 @@ func (svc *AlbyOAuthService) GetInfo(ctx context.Context, senderPubkey string) ( svc.Logger.WithFields(logrus.Fields{ "senderPubkey": senderPubkey, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, }).Info("Info fetch successful") return &NodeInfo{ Alias: "getalby.com", @@ -305,7 +318,7 @@ func (svc *AlbyOAuthService) GetInfo(ctx context.Context, senderPubkey string) ( func (svc *AlbyOAuthService) GetBalance(ctx context.Context, senderPubkey string) (balance int64, err error) { app := App{} - err = svc.db.Preload("User").First(&app, &App{ + err = svc.db.First(&app, &App{ NostrPubkey: senderPubkey, }).Error if err != nil { @@ -333,7 +346,7 @@ func (svc *AlbyOAuthService) GetBalance(ctx context.Context, senderPubkey string svc.Logger.WithFields(logrus.Fields{ "senderPubkey": senderPubkey, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, }).Errorf("Failed to fetch balance: %v", err) return 0, err } @@ -347,7 +360,7 @@ func (svc *AlbyOAuthService) GetBalance(ctx context.Context, senderPubkey string svc.Logger.WithFields(logrus.Fields{ "senderPubkey": senderPubkey, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, }).Info("Balance fetch successful") return int64(responsePayload.Balance), nil } @@ -357,7 +370,7 @@ func (svc *AlbyOAuthService) GetBalance(ctx context.Context, senderPubkey string svc.Logger.WithFields(logrus.Fields{ "senderPubkey": senderPubkey, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, "APIHttpStatus": resp.StatusCode, }).Errorf("Balance fetch failed %s", string(errorPayload.Message)) return 0, errors.New(errorPayload.Message) @@ -365,7 +378,7 @@ func (svc *AlbyOAuthService) GetBalance(ctx context.Context, senderPubkey string func (svc *AlbyOAuthService) ListTransactions(ctx context.Context, senderPubkey string, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []Nip47Transaction, err error) { app := App{} - err = svc.db.Preload("User").First(&app, &App{ + err = svc.db.First(&app, &App{ NostrPubkey: senderPubkey, }).Error if err != nil { @@ -419,7 +432,7 @@ func (svc *AlbyOAuthService) ListTransactions(ctx context.Context, senderPubkey svc.Logger.WithFields(logrus.Fields{ "senderPubkey": senderPubkey, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, "requestUrl": requestUrl, }).Errorf("Failed to fetch invoices: %v", err) return nil, err @@ -433,7 +446,7 @@ func (svc *AlbyOAuthService) ListTransactions(ctx context.Context, senderPubkey svc.Logger.WithFields(logrus.Fields{ "senderPubkey": senderPubkey, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, "requestUrl": requestUrl, }).Errorf("Failed to decode invoices: %v", err) return nil, err @@ -449,7 +462,7 @@ func (svc *AlbyOAuthService) ListTransactions(ctx context.Context, senderPubkey svc.Logger.WithFields(logrus.Fields{ "senderPubkey": senderPubkey, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, "requestUrl": requestUrl, }).Info("List transactions successful") return transactions, nil @@ -460,7 +473,7 @@ func (svc *AlbyOAuthService) ListTransactions(ctx context.Context, senderPubkey svc.Logger.WithFields(logrus.Fields{ "senderPubkey": senderPubkey, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, "APIHttpStatus": resp.StatusCode, "requestUrl": requestUrl, }).Errorf("List transactions failed %s", string(errorPayload.Message)) @@ -469,7 +482,7 @@ func (svc *AlbyOAuthService) ListTransactions(ctx context.Context, senderPubkey func (svc *AlbyOAuthService) SendPaymentSync(ctx context.Context, senderPubkey, payReq string) (preimage string, err error) { app := App{} - err = svc.db.Preload("User").First(&app, &App{ + err = svc.db.First(&app, &App{ NostrPubkey: senderPubkey, }).Error if err != nil { @@ -483,7 +496,7 @@ func (svc *AlbyOAuthService) SendPaymentSync(ctx context.Context, senderPubkey, "senderPubkey": senderPubkey, "bolt11": payReq, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, }).Info("Processing payment request") tok, err := svc.FetchUserToken(ctx, app) if err != nil { @@ -512,7 +525,7 @@ func (svc *AlbyOAuthService) SendPaymentSync(ctx context.Context, senderPubkey, "senderPubkey": senderPubkey, "bolt11": payReq, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, }).Errorf("Failed to pay invoice: %v", err) return "", err } @@ -527,7 +540,7 @@ func (svc *AlbyOAuthService) SendPaymentSync(ctx context.Context, senderPubkey, "senderPubkey": senderPubkey, "bolt11": payReq, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, "paymentHash": responsePayload.PaymentHash, }).Info("Payment successful") return responsePayload.Preimage, nil @@ -539,7 +552,7 @@ func (svc *AlbyOAuthService) SendPaymentSync(ctx context.Context, senderPubkey, "senderPubkey": senderPubkey, "bolt11": payReq, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, "APIHttpStatus": resp.StatusCode, }).Errorf("Payment failed %s", string(errorPayload.Message)) return "", errors.New(errorPayload.Message) @@ -547,7 +560,7 @@ func (svc *AlbyOAuthService) SendPaymentSync(ctx context.Context, senderPubkey, 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{ + err = svc.db.First(&app, &App{ NostrPubkey: senderPubkey, }).Error if err != nil { @@ -561,7 +574,7 @@ func (svc *AlbyOAuthService) SendKeysend(ctx context.Context, senderPubkey strin "senderPubkey": senderPubkey, "payeePubkey": destination, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, }).Info("Processing keysend request") tok, err := svc.FetchUserToken(ctx, app) if err != nil { @@ -598,7 +611,7 @@ func (svc *AlbyOAuthService) SendKeysend(ctx context.Context, senderPubkey strin "senderPubkey": senderPubkey, "payeePubkey": destination, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, }).Errorf("Failed to pay keysend: %v", err) return "", err } @@ -613,7 +626,7 @@ func (svc *AlbyOAuthService) SendKeysend(ctx context.Context, senderPubkey strin "senderPubkey": senderPubkey, "payeePubkey": destination, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, "preimage": responsePayload.Preimage, "paymentHash": responsePayload.PaymentHash, }).Info("Keysend payment successful") @@ -626,7 +639,7 @@ func (svc *AlbyOAuthService) SendKeysend(ctx context.Context, senderPubkey strin "senderPubkey": senderPubkey, "payeePubkey": destination, "appId": app.ID, - "userId": app.User.ID, + "userId": app.UserId, "APIHttpStatus": resp.StatusCode, }).Errorf("Payment failed %s", string(errorPayload.Message)) return "", errors.New(errorPayload.Message) From 451f7d6c64d93900ee9b789d5c60cfe2c6d77e3e Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 23 Jan 2024 11:40:24 +0700 Subject: [PATCH 07/12] fix: add one second to oauth token expiry check --- alby.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alby.go b/alby.go index f50c1bc5..1044e550 100644 --- a/alby.go +++ b/alby.go @@ -66,7 +66,7 @@ func (svc *AlbyOAuthService) FetchUserToken(ctx context.Context, app App) (token Expiry: user.Expiry, } - if user.Expiry.After(time.Now()) { + if user.Expiry.After(time.Now().Add(time.Duration(1) * time.Second)) { return currentToken, nil } From 1e3d7208a2ad407f386016e75bfe772a26378757 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 23 Jan 2024 11:59:34 +0700 Subject: [PATCH 08/12] chore: split handleUnknownMethod function, add TODO about inconsistent method handling --- service.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/service.go b/service.go index 03a6f2d3..03b8888e 100644 --- a/service.go +++ b/service.go @@ -214,6 +214,9 @@ func (svc *Service) handleAndPublishEvent(ctx context.Context, sub *nostr.Subscr }).Errorf("Failed to process event: %v", err) return } + + // TODO: consider move event publishing to individual methods - multi_* methods are + // inconsistent with single methods because they publish multiple responses switch nip47Request.Method { case NIP_47_MULTI_PAY_INVOICE_METHOD: svc.HandleMultiPayInvoiceEvent(ctx, sub, nip47Request, event, app, ss) @@ -233,12 +236,7 @@ func (svc *Service) handleAndPublishEvent(ctx context.Context, sub *nostr.Subscr case NIP_47_GET_INFO_METHOD: resp, err = svc.HandleGetInfoEvent(ctx, nip47Request, event, app, ss) default: - resp, err = svc.createResponse(event, Nip47Response{ - ResultType: nip47Request.Method, - Error: &Nip47Error{ - Code: NIP_47_ERROR_NOT_IMPLEMENTED, - Message: fmt.Sprintf("Unknown method: %s", nip47Request.Method), - }}, nostr.Tags{}, ss) + resp, err = svc.handleUnknownMethod(ctx, nip47Request, event, app, ss) } if err != nil { @@ -252,6 +250,15 @@ func (svc *Service) handleAndPublishEvent(ctx context.Context, sub *nostr.Subscr } } +func (svc *Service) handleUnknownMethod(ctx context.Context, request *Nip47Request, event *nostr.Event, app App, ss []byte) (result *nostr.Event, err error) { + return svc.createResponse(event, Nip47Response{ + ResultType: request.Method, + Error: &Nip47Error{ + Code: NIP_47_ERROR_NOT_IMPLEMENTED, + Message: fmt.Sprintf("Unknown method: %s", request.Method), + }}, nostr.Tags{}, ss) +} + func (svc *Service) createResponse(initialEvent *nostr.Event, content interface{}, tags nostr.Tags, ss []byte) (result *nostr.Event, err error) { payloadBytes, err := json.Marshal(content) if err != nil { From 3e8f41cb26bb827bf2bca2c00e7d1b5f3bbd8bbd Mon Sep 17 00:00:00 2001 From: im-adithya Date: Wed, 24 Jan 2024 21:25:57 +0530 Subject: [PATCH 09/12] chore: fix tests --- service.go | 4 +- service_test.go | 769 ++++++++++++++++++++++++++---------------------- 2 files changed, 426 insertions(+), 347 deletions(-) diff --git a/service.go b/service.go index 03b8888e..368310a5 100644 --- a/service.go +++ b/service.go @@ -61,7 +61,7 @@ func (svc *Service) StartSubscription(ctx context.Context, sub *nostr.Subscripti svc.Logger.Info("Received EOS") for event := range sub.Events { - go svc.handleAndPublishEvent(ctx, sub, event) + go svc.HandleEvent(ctx, sub, event) } svc.Logger.Info("Subscription ended") }() @@ -137,7 +137,7 @@ func (svc *Service) PublishEvent(ctx context.Context, sub *nostr.Subscription, e } } -func (svc *Service) handleAndPublishEvent(ctx context.Context, sub *nostr.Subscription, event *nostr.Event) { +func (svc *Service) HandleEvent(ctx context.Context, sub *nostr.Subscription, event *nostr.Event) { var resp *nostr.Event svc.Logger.WithFields(logrus.Fields{ "eventId": event.ID, diff --git a/service_test.go b/service_test.go index 8cc49e45..e28c4ad7 100644 --- a/service_test.go +++ b/service_test.go @@ -3,6 +3,7 @@ package main import ( "context" "encoding/json" + "fmt" "os" "testing" "time" @@ -64,7 +65,7 @@ const nip47KeysendJson = ` { "method": "pay_keysend", "params": { - "amount": 100, + "amount": 123000, "pubkey": "123pubkey", "tlv_records": [{ "type": 5482373484, @@ -143,114 +144,133 @@ var mockTransactions = []Nip47Transaction{ } var mockTransaction = &mockTransactions[0] -// TODO: split up into individual tests -func TestHandleEvent(t *testing.T) { +func TestBackwardsCompatibility(t *testing.T) { + // Test without adding a single permission +} + +func TestHandleGetBalanceEvent(t *testing.T) { ctx := context.TODO() - svc, _ := createTestService(t) defer os.Remove(testDB) - //test not yet receivedEOS - res, err := svc.HandleEvent(ctx, &nostr.Event{ - Kind: NIP_47_REQUEST_KIND, - }) - assert.Nil(t, res) - assert.Nil(t, err) - //now signal that we are ready to receive events - svc.ReceivedEOS = true - - senderPrivkey := nostr.GeneratePrivateKey() - senderPubkey, err := nostr.GetPublicKey(senderPrivkey) + mockLn, err := NewMockLn() assert.NoError(t, err) - //test lnbc.. payload without having an app registered - ss, err := nip04.ComputeSharedSecret(svc.cfg.IdentityPubkey, senderPrivkey) + svc, err := createTestService(mockLn) assert.NoError(t, err) - payload, err := nip04.Encrypt(nip47PayJson, ss) + _, app, ss, err := createUserWithApp(svc) + assert.NoError(t, err) + + request := &Nip47Request{} + err = json.Unmarshal([]byte(nip47GetBalanceJson), request) assert.NoError(t, err) - res, err = svc.HandleEvent(ctx, &nostr.Event{ - ID: "test_event_1", + + // without permission + payload, err := nip04.Encrypt(nip47GetBalanceJson, ss) + assert.NoError(t, err) + reqEvent := &nostr.Event{ Kind: NIP_47_REQUEST_KIND, - PubKey: senderPubkey, + PubKey: app.NostrPubkey, Content: payload, - }) + } + + reqEvent.ID = "test_get_balance_without_permission" + res, err := svc.HandleGetBalanceEvent(ctx, request, reqEvent, *app, ss) assert.NoError(t, err) assert.NotNil(t, res) - received := &Nip47Response{} - decrypted, err := nip04.Decrypt(res.Content, ss) + + response, err := decryptResponse(res, ss, nil) assert.NoError(t, err) - err = json.Unmarshal([]byte(decrypted), received) + assert.NotNil(t, response) + assert.Equal(t, NIP_47_ERROR_RESTRICTED, response.Error.Code) + + // with permission + expiresAt := time.Now().Add(24 * time.Hour) + appPermission := &AppPermission{ + AppId: app.ID, + App: *app, + RequestMethod: NIP_47_GET_BALANCE_METHOD, + ExpiresAt: expiresAt, + } + err = svc.db.Create(appPermission).Error + assert.NoError(t, err) + + reqEvent.ID = "test_get_balance_with_permission" + res, err = svc.HandleGetBalanceEvent(ctx, request, reqEvent, *app, ss) assert.NoError(t, err) - assert.Equal(t, received.Error.Code, NIP_47_ERROR_UNAUTHORIZED) assert.NotNil(t, res) - //create user - user := &User{ID: 0, AlbyIdentifier: "dummy"} - err = svc.db.Create(user).Error + + response, err = decryptResponse(res, ss, &Nip47BalanceResponse{}) assert.NoError(t, err) - //register app - app := App{Name: "test", NostrPubkey: senderPubkey} - err = svc.db.Model(&user).Association("Apps").Append(&app) + assert.NotNil(t, response) + + assert.Equal(t, int64(21000), response.Result.(*Nip47BalanceResponse).Balance) + + // create pay_invoice permission + maxAmount := 1000 + budgetRenewal := "never" + appPermission = &AppPermission{ + AppId: app.ID, + App: *app, + RequestMethod: NIP_47_PAY_INVOICE_METHOD, + MaxAmount: maxAmount, + BudgetRenewal: budgetRenewal, + ExpiresAt: expiresAt, + } + err = svc.db.Create(appPermission).Error assert.NoError(t, err) - //test old payload - res, err = svc.HandleEvent(ctx, &nostr.Event{ - ID: "test_event_2", - Kind: NIP_47_REQUEST_KIND, - PubKey: senderPubkey, - Content: payload, - }) + + reqEvent.ID = "test_get_balance_with_budget" + res, err = svc.HandleGetBalanceEvent(ctx, request, reqEvent, *app, ss) assert.NoError(t, err) assert.NotNil(t, res) - //test new payload - newPayload, err := nip04.Encrypt(nip47PayJson, ss) + + response, err = decryptResponse(res, ss, &Nip47BalanceResponse{}) assert.NoError(t, err) - res, err = svc.HandleEvent(ctx, &nostr.Event{ - ID: "test_event_3", - Kind: NIP_47_REQUEST_KIND, - PubKey: senderPubkey, - Content: newPayload, - }) + assert.NotNil(t, response) + + assert.Equal(t, int64(21000), response.Result.(*Nip47BalanceResponse).Balance) + assert.Equal(t, 1000000, response.Result.(*Nip47BalanceResponse).MaxAmount) + assert.Equal(t, "never", response.Result.(*Nip47BalanceResponse).BudgetRenewal) +} + +func TestHandlePayInvoiceEvent(t *testing.T) { + ctx := context.TODO() + defer os.Remove(testDB) + mockLn, err := NewMockLn() assert.NoError(t, err) - assert.NotNil(t, res) - decrypted, err = nip04.Decrypt(res.Content, ss) + svc, err := createTestService(mockLn) assert.NoError(t, err) - received = &Nip47Response{ - Result: &Nip47PayResponse{}, - } - err = json.Unmarshal([]byte(decrypted), received) + _, app, ss, err := createUserWithApp(svc) assert.NoError(t, err) - assert.Equal(t, received.Result.(*Nip47PayResponse).Preimage, "123preimage") - malformedPayload, err := nip04.Encrypt(nip47PayJsonNoInvoice, ss) + + request := &Nip47Request{} + err = json.Unmarshal([]byte(nip47PayJson), request) assert.NoError(t, err) - res, err = svc.HandleEvent(ctx, &nostr.Event{ - ID: "test_event_4", - Kind: NIP_47_REQUEST_KIND, - PubKey: senderPubkey, - Content: malformedPayload, - }) - decrypted, err = nip04.Decrypt(res.Content, ss) + // without permission + payload, err := nip04.Encrypt(nip47PayJson, ss) assert.NoError(t, err) - receivedError := &Nip47Response{ - Result: &Nip47Error{}, + reqEvent := &nostr.Event{ + Kind: NIP_47_REQUEST_KIND, + PubKey: app.NostrPubkey, + Content: payload, } - err = json.Unmarshal([]byte(decrypted), receivedError) - assert.NoError(t, err) - assert.Equal(t, NIP_47_ERROR_INTERNAL, receivedError.Error.Code) - //test wrong method - wrongMethodPayload, err := nip04.Encrypt(nip47PayWrongMethodJson, ss) + reqEvent.ID = "pay_invoice_without_permission" + res, err := svc.HandlePayInvoiceEvent(ctx, request, reqEvent, *app, ss) assert.NoError(t, err) - res, err = svc.HandleEvent(ctx, &nostr.Event{ - ID: "test_event_5", - Kind: NIP_47_REQUEST_KIND, - PubKey: senderPubkey, - Content: wrongMethodPayload, - }) + assert.NotNil(t, res) + + response, err := decryptResponse(res, ss, nil) assert.NoError(t, err) - //add app permissions + assert.NotNil(t, response) + assert.Equal(t, NIP_47_ERROR_RESTRICTED, response.Error.Code) + + // with permission maxAmount := 1000 budgetRenewal := "never" expiresAt := time.Now().Add(24 * time.Hour) appPermission := &AppPermission{ AppId: app.ID, - App: app, + App: *app, RequestMethod: NIP_47_PAY_INVOICE_METHOD, MaxAmount: maxAmount, BudgetRenewal: budgetRenewal, @@ -258,312 +278,299 @@ func TestHandleEvent(t *testing.T) { } err = svc.db.Create(appPermission).Error assert.NoError(t, err) - // permissions: no limitations - res, err = svc.HandleEvent(ctx, &nostr.Event{ - ID: "test_event_6", - Kind: NIP_47_REQUEST_KIND, - PubKey: senderPubkey, - Content: newPayload, - }) + + reqEvent.ID = "pay_invoice_with_permission" + res, err = svc.HandlePayInvoiceEvent(ctx, request, reqEvent, *app, ss) 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) - // permissions: budget overflow - newMaxAmount := 100 - err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("max_amount", newMaxAmount).Error - res, err = svc.HandleEvent(ctx, &nostr.Event{ - ID: "test_event_7", - Kind: NIP_47_REQUEST_KIND, - PubKey: senderPubkey, - Content: newPayload, - }) + response, err = decryptResponse(res, ss, &Nip47PayResponse{}) assert.NoError(t, err) - assert.NotNil(t, res) + assert.NotNil(t, response) + assert.Equal(t, response.Result.(*Nip47PayResponse).Preimage, "123preimage") - decrypted, err = nip04.Decrypt(res.Content, ss) + // malformed invoice + err = json.Unmarshal([]byte(nip47PayJsonNoInvoice), request) assert.NoError(t, err) - received = &Nip47Response{} - err = json.Unmarshal([]byte(decrypted), received) + + payload, err = nip04.Encrypt(nip47PayJsonNoInvoice, ss) assert.NoError(t, err) - assert.Equal(t, NIP_47_ERROR_QUOTA_EXCEEDED, received.Error.Code) - assert.NotNil(t, res) - // permissions: expired app - newExpiry := time.Now().Add(-24 * time.Hour) - err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("expires_at", newExpiry).Error + reqEvent.Content = payload - res, err = svc.HandleEvent(ctx, &nostr.Event{ - ID: "test_event_8", - Kind: NIP_47_REQUEST_KIND, - PubKey: senderPubkey, - Content: newPayload, - }) + reqEvent.ID = "pay_invoice_with_malformed_invoice" + res, err = svc.HandlePayInvoiceEvent(ctx, request, reqEvent, *app, ss) assert.NoError(t, err) assert.NotNil(t, res) - decrypted, err = nip04.Decrypt(res.Content, ss) + response, err = decryptResponse(res, ss, &Nip47Error{}) assert.NoError(t, err) - received = &Nip47Response{} - err = json.Unmarshal([]byte(decrypted), received) + assert.NotNil(t, response) + assert.Equal(t, NIP_47_ERROR_INTERNAL, response.Error.Code) + + // wrong method + err = json.Unmarshal([]byte(nip47PayWrongMethodJson), request) assert.NoError(t, err) - assert.Equal(t, NIP_47_ERROR_EXPIRED, received.Error.Code) - assert.NotNil(t, res) - // remove expiry - err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("expires_at", nil).Error - // permissions: no request method - err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("request_method", nil).Error + payload, err = nip04.Encrypt(nip47PayWrongMethodJson, ss) + assert.NoError(t, err) + reqEvent.Content = payload - res, err = svc.HandleEvent(ctx, &nostr.Event{ - ID: "test_event_9", - Kind: NIP_47_REQUEST_KIND, - PubKey: senderPubkey, - Content: newPayload, - }) + reqEvent.ID = "pay_invoice_with_wrong_request_method" + res, err = svc.HandlePayInvoiceEvent(ctx, request, reqEvent, *app, ss) assert.NoError(t, err) assert.NotNil(t, res) - decrypted, err = nip04.Decrypt(res.Content, ss) + response, err = decryptResponse(res, ss, &Nip47Error{}) assert.NoError(t, err) - received = &Nip47Response{} - err = json.Unmarshal([]byte(decrypted), received) + assert.NotNil(t, response) + assert.Equal(t, NIP_47_ERROR_RESTRICTED, response.Error.Code) + + // budget overflow + newMaxAmount := 100 + err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("max_amount", newMaxAmount).Error + + err = json.Unmarshal([]byte(nip47PayJson), request) 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) + payload, err = nip04.Encrypt(nip47PayJson, 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, - }) + reqEvent.Content = payload + + reqEvent.ID = "pay_invoice_with_budget_overflow" + res, err = svc.HandlePayInvoiceEvent(ctx, request, reqEvent, *app, ss) assert.NoError(t, err) assert.NotNil(t, res) - decrypted, err = nip04.Decrypt(res.Content, ss) + + response, err = decryptResponse(res, ss, &Nip47Error{}) assert.NoError(t, err) - received = &Nip47Response{} - err = json.Unmarshal([]byte(decrypted), received) + assert.NotNil(t, response) + assert.Equal(t, NIP_47_ERROR_QUOTA_EXCEEDED, response.Error.Code) + + // budget expiry + newExpiry := time.Now().Add(-24 * time.Hour) + err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("max_amount", maxAmount).Update("expires_at", newExpiry).Error + + reqEvent.ID = "pay_invoice_with_budget_expiry" + res, err = svc.HandlePayInvoiceEvent(ctx, request, reqEvent, *app, ss) 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 + + response, err = decryptResponse(res, ss, &Nip47Error{}) assert.NoError(t, err) - 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.NotNil(t, response) + assert.Equal(t, NIP_47_ERROR_EXPIRED, response.Error.Code) + + // check again + err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("expires_at", nil).Error + + reqEvent.ID = "pay_invoice_with_budget_overflow" + res, err = svc.HandlePayInvoiceEvent(ctx, request, reqEvent, *app, ss) 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) + + response, err = decryptResponse(res, ss, &Nip47PayResponse{}) assert.NoError(t, err) - assert.Equal(t, "123preimage", received.Result.(*Nip47PayResponse).Preimage) + assert.NotNil(t, response) + assert.Equal(t, response.Result.(*Nip47PayResponse).Preimage, "123preimage") +} - // 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, - }) +func TestHandlePayKeysendEvent(t *testing.T) { + ctx := context.TODO() + defer os.Remove(testDB) + mockLn, err := NewMockLn() assert.NoError(t, err) - assert.NotNil(t, res) - decrypted, err = nip04.Decrypt(res.Content, ss) + svc, err := createTestService(mockLn) assert.NoError(t, err) - received = &Nip47Response{ - Result: &Nip47PayResponse{}, - } - err = json.Unmarshal([]byte(decrypted), received) + _, app, ss, err := createUserWithApp(svc) + assert.NoError(t, err) + + request := &Nip47Request{} + err = json.Unmarshal([]byte(nip47KeysendJson), request) 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) + // without permission + payload, err := nip04.Encrypt(nip47KeysendJson, ss) assert.NoError(t, err) - res, err = svc.HandleEvent(ctx, &nostr.Event{ - ID: "test_event_10", + reqEvent := &nostr.Event{ Kind: NIP_47_REQUEST_KIND, - PubKey: senderPubkey, - Content: newPayload, - }) + PubKey: app.NostrPubkey, + Content: payload, + } + + reqEvent.ID = "pay_keysend_without_permission" + res, err := svc.HandlePayKeysendEvent(ctx, request, reqEvent, *app, ss) 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) + + response, err := decryptResponse(res, ss, nil) assert.NoError(t, err) - assert.Equal(t, NIP_47_ERROR_RESTRICTED, received.Error.Code) - assert.NotNil(t, res) - // get_balance: with permission - // the pay_invoice permmission already exists with the budget info - // create a second permission to fetch the balance and budget info - appPermission = &AppPermission{ + assert.NotNil(t, response) + assert.Equal(t, NIP_47_ERROR_RESTRICTED, response.Error.Code) + + // with permission + maxAmount := 1000 + budgetRenewal := "never" + expiresAt := time.Now().Add(24 * time.Hour) + // because we need the same permission for keysend although + // it works even with NIP_47_PAY_KEYSEND_METHOD, see + // https://github.com/getAlby/nostr-wallet-connect/issues/189 + appPermission := &AppPermission{ AppId: app.ID, - App: app, - RequestMethod: NIP_47_GET_BALANCE_METHOD, + App: *app, + RequestMethod: NIP_47_PAY_INVOICE_METHOD, + MaxAmount: maxAmount, + BudgetRenewal: budgetRenewal, ExpiresAt: expiresAt, } err = svc.db.Create(appPermission).Error - res, err = svc.HandleEvent(ctx, &nostr.Event{ - ID: "test_event_11", - Kind: NIP_47_REQUEST_KIND, - PubKey: senderPubkey, - Content: newPayload, - }) + assert.NoError(t, err) + + reqEvent.ID = "pay_keysend_with_permission" + res, err = svc.HandlePayKeysendEvent(ctx, request, reqEvent, *app, ss) assert.NoError(t, err) assert.NotNil(t, res) - decrypted, err = nip04.Decrypt(res.Content, ss) + + response, err = decryptResponse(res, ss, &Nip47PayResponse{}) assert.NoError(t, err) - received = &Nip47Response{ - Result: &Nip47BalanceResponse{}, - } - err = json.Unmarshal([]byte(decrypted), received) + assert.NotNil(t, response) + assert.Equal(t, response.Result.(*Nip47PayResponse).Preimage, "12345preimage") + + // budget overflow + newMaxAmount := 100 + err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("max_amount", newMaxAmount).Error + + err = json.Unmarshal([]byte(nip47KeysendJson), request) assert.NoError(t, err) - assert.Equal(t, int64(21000), received.Result.(*Nip47BalanceResponse).Balance) - assert.Equal(t, 100000, received.Result.(*Nip47BalanceResponse).MaxAmount) - assert.Equal(t, "never", received.Result.(*Nip47BalanceResponse).BudgetRenewal) - // make_invoice: without permission - newPayload, err = nip04.Encrypt(nip47MakeInvoiceJson, ss) + payload, err = nip04.Encrypt(nip47KeysendJson, ss) assert.NoError(t, err) - res, err = svc.HandleEvent(ctx, &nostr.Event{ - ID: "test_event_12", - Kind: NIP_47_REQUEST_KIND, - PubKey: senderPubkey, - Content: newPayload, - }) + reqEvent.Content = payload + + reqEvent.ID = "pay_keysend_with_budget_overflow" + res, err = svc.HandlePayKeysendEvent(ctx, request, reqEvent, *app, ss) 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) + + response, err = decryptResponse(res, ss, &Nip47Error{}) + fmt.Println("response") + fmt.Println(response.Result) + fmt.Println("response") assert.NoError(t, err) - assert.Equal(t, NIP_47_ERROR_RESTRICTED, received.Error.Code) - assert.NotNil(t, res) + assert.NotNil(t, response) + assert.Equal(t, NIP_47_ERROR_QUOTA_EXCEEDED, response.Error.Code) +} - // 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", - Kind: NIP_47_REQUEST_KIND, - PubKey: senderPubkey, - Content: newPayload, - }) +func TestHandleMakeInvoiceEvent(t *testing.T) { + ctx := context.TODO() + defer os.Remove(testDB) + mockLn, err := NewMockLn() assert.NoError(t, err) - assert.NotNil(t, res) - decrypted, err = nip04.Decrypt(res.Content, ss) + svc, err := createTestService(mockLn) assert.NoError(t, err) - received = &Nip47Response{ - Result: &Nip47MakeInvoiceResponse{}, - } - err = json.Unmarshal([]byte(decrypted), received) + _, app, ss, err := createUserWithApp(svc) + assert.NoError(t, err) + + request := &Nip47Request{} + err = json.Unmarshal([]byte(nip47MakeInvoiceJson), request) assert.NoError(t, err) - assert.Equal(t, mockTransaction.Preimage, received.Result.(*Nip47MakeInvoiceResponse).Preimage) - // lookup_invoice: without permission - newPayload, err = nip04.Encrypt(nip47LookupInvoiceJson, ss) + // without permission + payload, err := nip04.Encrypt(nip47MakeInvoiceJson, ss) assert.NoError(t, err) - res, err = svc.HandleEvent(ctx, &nostr.Event{ - ID: "test_event_14", + reqEvent := &nostr.Event{ Kind: NIP_47_REQUEST_KIND, - PubKey: senderPubkey, - Content: newPayload, - }) + PubKey: app.NostrPubkey, + Content: payload, + } + + reqEvent.ID = "test_make_invoice_without_permission" + res, err := svc.HandleMakeInvoiceEvent(ctx, request, reqEvent, *app, ss) 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) + + response, err := decryptResponse(res, ss, nil) assert.NoError(t, err) - assert.Equal(t, NIP_47_ERROR_RESTRICTED, received.Error.Code) - assert.NotNil(t, res) + assert.NotNil(t, response) + assert.Equal(t, NIP_47_ERROR_RESTRICTED, response.Error.Code) - // lookup_invoice: with permission - err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("request_method", NIP_47_LOOKUP_INVOICE_METHOD).Error + // with permission + expiresAt := time.Now().Add(24 * time.Hour) + appPermission := &AppPermission{ + AppId: app.ID, + App: *app, + RequestMethod: NIP_47_MAKE_INVOICE_METHOD, + ExpiresAt: expiresAt, + } + err = svc.db.Create(appPermission).Error assert.NoError(t, err) - res, err = svc.HandleEvent(ctx, &nostr.Event{ - ID: "test_event_15", - Kind: NIP_47_REQUEST_KIND, - PubKey: senderPubkey, - Content: newPayload, - }) + + reqEvent.ID = "test_make_invoice_with_permission" + res, err = svc.HandleMakeInvoiceEvent(ctx, request, reqEvent, *app, ss) assert.NoError(t, err) assert.NotNil(t, res) - decrypted, err = nip04.Decrypt(res.Content, ss) - assert.NoError(t, err) - received = &Nip47Response{ - Result: &Nip47LookupInvoiceResponse{}, - } - err = json.Unmarshal([]byte(decrypted), received) + + response, err = decryptResponse(res, ss, &Nip47MakeInvoiceResponse{}) assert.NoError(t, err) - assert.Equal(t, mockTransaction.Preimage, received.Result.(*Nip47LookupInvoiceResponse).Preimage) + assert.NotNil(t, response) + + assert.Equal(t, mockTransaction.Preimage, response.Result.(*Nip47MakeInvoiceResponse).Preimage) +} - // list_transactions: without permission - newPayload, err = nip04.Encrypt(nip47ListTransactionsJson, ss) +func TestHandleListTransactionsEvent(t *testing.T) { + ctx := context.TODO() + defer os.Remove(testDB) + mockLn, err := NewMockLn() assert.NoError(t, err) - res, err = svc.HandleEvent(ctx, &nostr.Event{ - ID: "test_list_transactions_event_1", - Kind: NIP_47_REQUEST_KIND, - PubKey: senderPubkey, - Content: newPayload, - }) + svc, err := createTestService(mockLn) assert.NoError(t, err) - assert.NotNil(t, res) - decrypted, err = nip04.Decrypt(res.Content, ss) + _, app, ss, err := createUserWithApp(svc) assert.NoError(t, err) - received = &Nip47Response{} - err = json.Unmarshal([]byte(decrypted), received) + + request := &Nip47Request{} + err = json.Unmarshal([]byte(nip47ListTransactionsJson), request) assert.NoError(t, err) - assert.Equal(t, NIP_47_ERROR_RESTRICTED, received.Error.Code) - assert.NotNil(t, res) - // list_transactions: with permission - err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("request_method", NIP_47_LIST_TRANSACTIONS_METHOD).Error + // without permission + payload, err := nip04.Encrypt(nip47ListTransactionsJson, ss) assert.NoError(t, err) - res, err = svc.HandleEvent(ctx, &nostr.Event{ - ID: "test_list_transactions_event_2", + reqEvent := &nostr.Event{ Kind: NIP_47_REQUEST_KIND, - PubKey: senderPubkey, - Content: newPayload, - }) + PubKey: app.NostrPubkey, + Content: payload, + } + + reqEvent.ID = "test_list_transactions_without_permission" + res, err := svc.HandleListTransactionsEvent(ctx, request, reqEvent, *app, ss) assert.NoError(t, err) assert.NotNil(t, res) - decrypted, err = nip04.Decrypt(res.Content, ss) + + response, err := decryptResponse(res, ss, nil) assert.NoError(t, err) - received = &Nip47Response{ - Result: &Nip47ListTransactionsResponse{}, + assert.NotNil(t, response) + assert.Equal(t, NIP_47_ERROR_RESTRICTED, response.Error.Code) + + // with permission + expiresAt := time.Now().Add(24 * time.Hour) + appPermission := &AppPermission{ + AppId: app.ID, + App: *app, + RequestMethod: NIP_47_LIST_TRANSACTIONS_METHOD, + ExpiresAt: expiresAt, } - err = json.Unmarshal([]byte(decrypted), received) + err = svc.db.Create(appPermission).Error + assert.NoError(t, err) + + reqEvent.ID = "test_list_transactions_with_permission" + res, err = svc.HandleListTransactionsEvent(ctx, request, reqEvent, *app, ss) + assert.NoError(t, err) + assert.NotNil(t, res) + + response, err = decryptResponse(res, ss, &Nip47ListTransactionsResponse{}) assert.NoError(t, err) - assert.Equal(t, 2, len(received.Result.(*Nip47ListTransactionsResponse).Transactions)) - transaction := received.Result.(*Nip47ListTransactionsResponse).Transactions[0] + assert.NotNil(t, response) + + assert.Equal(t, 2, len(response.Result.(*Nip47ListTransactionsResponse).Transactions)) + transaction := response.Result.(*Nip47ListTransactionsResponse).Transactions[0] assert.Equal(t, mockTransactions[0].Type, transaction.Type) assert.Equal(t, mockTransactions[0].Invoice, transaction.Invoice) assert.Equal(t, mockTransactions[0].Description, transaction.Description) @@ -573,70 +580,87 @@ func TestHandleEvent(t *testing.T) { assert.Equal(t, mockTransactions[0].Amount, transaction.Amount) assert.Equal(t, mockTransactions[0].FeesPaid, transaction.FeesPaid) assert.Equal(t, mockTransactions[0].SettledAt, transaction.SettledAt) +} - // get_info: without permission - newPayload, err = nip04.Encrypt(nip47GetInfoJson, ss) +func TestHandleGetInfoEvent(t *testing.T) { + ctx := context.TODO() + defer os.Remove(testDB) + mockLn, err := NewMockLn() assert.NoError(t, err) - res, err = svc.HandleEvent(ctx, &nostr.Event{ - ID: "test_event_16", - Kind: NIP_47_REQUEST_KIND, - PubKey: senderPubkey, - Content: newPayload, - }) + svc, err := createTestService(mockLn) assert.NoError(t, err) - assert.NotNil(t, res) - decrypted, err = nip04.Decrypt(res.Content, ss) + _, app, ss, err := createUserWithApp(svc) assert.NoError(t, err) - received = &Nip47Response{} - err = json.Unmarshal([]byte(decrypted), received) + + request := &Nip47Request{} + err = json.Unmarshal([]byte(nip47GetInfoJson), request) + assert.NoError(t, err) + + // without permission + payload, err := nip04.Encrypt(nip47GetInfoJson, ss) + assert.NoError(t, err) + reqEvent := &nostr.Event{ + Kind: NIP_47_REQUEST_KIND, + PubKey: app.NostrPubkey, + Content: payload, + } + + reqEvent.ID = "test_get_info_without_permission" + res, err := svc.HandleGetInfoEvent(ctx, request, reqEvent, *app, ss) assert.NoError(t, err) - assert.Equal(t, NIP_47_ERROR_RESTRICTED, received.Error.Code) assert.NotNil(t, res) - // delete all permissions - svc.db.Exec("delete from app_permissions") + response, err := decryptResponse(res, ss, nil) + assert.NoError(t, err) + assert.NotNil(t, response) + assert.Equal(t, NIP_47_ERROR_RESTRICTED, response.Error.Code) - // get_info: with permission - appPermission = &AppPermission{ + // with permission + err = svc.db.Exec("delete from app_permissions").Error + assert.NoError(t, err) + + expiresAt := time.Now().Add(24 * time.Hour) + appPermission := &AppPermission{ AppId: app.ID, - App: app, + App: *app, RequestMethod: NIP_47_GET_INFO_METHOD, ExpiresAt: expiresAt, } err = svc.db.Create(appPermission).Error - res, err = svc.HandleEvent(ctx, &nostr.Event{ - ID: "test_event_17", - Kind: NIP_47_REQUEST_KIND, - PubKey: senderPubkey, - Content: newPayload, - }) + assert.NoError(t, err) + + reqEvent.ID = "test_get_info_with_permission" + res, err = svc.HandleGetInfoEvent(ctx, request, reqEvent, *app, ss) assert.NoError(t, err) assert.NotNil(t, res) - decrypted, err = nip04.Decrypt(res.Content, ss) + + response, err = decryptResponse(res, ss, &Nip47GetInfoResponse{}) assert.NoError(t, err) - received = &Nip47Response{ - Result: &Nip47GetInfoResponse{}, - } - err = json.Unmarshal([]byte(decrypted), received) - assert.NoError(t, err) - assert.Equal(t, mockNodeInfo.Alias, received.Result.(*Nip47GetInfoResponse).Alias) - assert.Equal(t, mockNodeInfo.Color, received.Result.(*Nip47GetInfoResponse).Color) - assert.Equal(t, mockNodeInfo.Pubkey, received.Result.(*Nip47GetInfoResponse).Pubkey) - assert.Equal(t, mockNodeInfo.Network, received.Result.(*Nip47GetInfoResponse).Network) - assert.Equal(t, mockNodeInfo.BlockHeight, received.Result.(*Nip47GetInfoResponse).BlockHeight) - assert.Equal(t, mockNodeInfo.BlockHash, received.Result.(*Nip47GetInfoResponse).BlockHash) - assert.Equal(t, []string{"get_info"}, received.Result.(*Nip47GetInfoResponse).Methods) + assert.NotNil(t, response) + + assert.Equal(t, mockNodeInfo.Alias, response.Result.(*Nip47GetInfoResponse).Alias) + assert.Equal(t, mockNodeInfo.Color, response.Result.(*Nip47GetInfoResponse).Color) + assert.Equal(t, mockNodeInfo.Pubkey, response.Result.(*Nip47GetInfoResponse).Pubkey) + assert.Equal(t, mockNodeInfo.Network, response.Result.(*Nip47GetInfoResponse).Network) + assert.Equal(t, mockNodeInfo.BlockHeight, response.Result.(*Nip47GetInfoResponse).BlockHeight) + assert.Equal(t, mockNodeInfo.BlockHash, response.Result.(*Nip47GetInfoResponse).BlockHash) + assert.Equal(t, []string{"get_info"}, response.Result.(*Nip47GetInfoResponse).Methods) } -func createTestService(t *testing.T) (svc *Service, ln *MockLn) { +func createTestService(ln *MockLn) (svc *Service, err error) { db, err := gorm.Open(sqlite.Open(testDB), &gorm.Config{}) - assert.NoError(t, err) + if err != nil { + return nil, err + } err = db.AutoMigrate(&User{}, &App{}, &AppPermission{}, &NostrEvent{}, &Payment{}, &Identity{}) - assert.NoError(t, err) - ln = &MockLn{} + if err != nil { + return nil, err + } sk := nostr.GeneratePrivateKey() pk, err := nostr.GetPublicKey(sk) - assert.NoError(t, err) + if err != nil { + return nil, err + } logger := logrus.New() logger.SetFormatter(&logrus.JSONFormatter{}) @@ -652,18 +676,73 @@ func createTestService(t *testing.T) (svc *Service, ln *MockLn) { lnClient: ln, ReceivedEOS: false, Logger: logger, - }, ln + }, nil +} + +func createUserWithApp(svc *Service) (user *User, app *App, ss []byte, err error) { + user = &User{ID: 0, AlbyIdentifier: "dummy"} + err = svc.db.Create(user).Error + if err != nil { + return nil, nil, nil, err + } + + senderPrivkey := nostr.GeneratePrivateKey() + senderPubkey, err := nostr.GetPublicKey(senderPrivkey) + + ss, err = nip04.ComputeSharedSecret(svc.cfg.IdentityPubkey, senderPrivkey) + if err != nil { + return nil, nil, nil, err + } + + app = &App{Name: "test", NostrPubkey: senderPubkey} + if err != nil { + return nil, nil, nil, err + } + err = svc.db.Model(&user).Association("Apps").Append(app) + if err != nil { + return nil, nil, nil, err + } + + // creating this permission because if no permissions + // are created for an app, it can do anything + appPermission := &AppPermission{ + AppId: app.ID, + App: *app, + RequestMethod: "UNKNOWN_METHOD", + } + err = svc.db.Create(appPermission).Error + + return user, app, ss, nil +} + +func decryptResponse(res *nostr.Event, ss []byte, resultType interface{}) (resp *Nip47Response, err error) { + decrypted, err := nip04.Decrypt(res.Content, ss) + if err != nil { + return nil, err + } + response := &Nip47Response{ + Result: resultType, + } + err = json.Unmarshal([]byte(decrypted), response) + if err != nil { + return nil, err + } + return response, nil } type MockLn struct { } +func NewMockLn() (*MockLn, error) { + return &MockLn{}, nil +} + func (mln *MockLn) SendPaymentSync(ctx context.Context, senderPubkey string, payReq string) (preimage string, err error) { return "123preimage", nil } func (mln *MockLn) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, preimage string, custom_records []TLVRecord) (preImage string, err error) { - return "123preimage", nil + return "12345preimage", nil } func (mln *MockLn) GetBalance(ctx context.Context, senderPubkey string) (balance int64, err error) { From 560b2265fa075251715aad253fadc6b49afd7ca8 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Thu, 25 Jan 2024 12:44:30 +0530 Subject: [PATCH 10/12] chore: add todos --- service.go | 2 ++ service_test.go | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/service.go b/service.go index 368310a5..25092f80 100644 --- a/service.go +++ b/service.go @@ -221,6 +221,8 @@ func (svc *Service) HandleEvent(ctx context.Context, sub *nostr.Subscription, ev case NIP_47_MULTI_PAY_INVOICE_METHOD: svc.HandleMultiPayInvoiceEvent(ctx, sub, nip47Request, event, app, ss) return + // TODO: for the below handlers consider returning + // Nip47Response instead of *nostr.Event case NIP_47_PAY_INVOICE_METHOD: resp, err = svc.HandlePayInvoiceEvent(ctx, nip47Request, event, app, ss) case NIP_47_PAY_KEYSEND_METHOD: diff --git a/service_test.go b/service_test.go index e28c4ad7..6ad25997 100644 --- a/service_test.go +++ b/service_test.go @@ -148,6 +148,10 @@ func TestBackwardsCompatibility(t *testing.T) { // Test without adding a single permission } +// TODO: add tests for HandleEvent method as these +// only cover the cases after the event is processed +// and the corresponding app is found + func TestHandleGetBalanceEvent(t *testing.T) { ctx := context.TODO() defer os.Remove(testDB) From b86ff49399f56ed967d5e3c614f39cb5015d6351 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Thu, 25 Jan 2024 13:25:31 +0530 Subject: [PATCH 11/12] fix: same event id in tests --- service_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service_test.go b/service_test.go index 8ad9a232..e9ecab24 100644 --- a/service_test.go +++ b/service_test.go @@ -368,7 +368,7 @@ func TestHandlePayInvoiceEvent(t *testing.T) { // check again err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("expires_at", nil).Error - reqEvent.ID = "pay_invoice_with_budget_overflow" + reqEvent.ID = "pay_invoice_after_change" res, err = svc.HandlePayInvoiceEvent(ctx, request, reqEvent, *app, ss) assert.NoError(t, err) assert.NotNil(t, res) From 01786f85c465e32ea3b9675a61a3a15e545eff57 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Thu, 25 Jan 2024 16:51:49 +0530 Subject: [PATCH 12/12] chore: remove test for backwards compatibility --- service_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/service_test.go b/service_test.go index e9ecab24..2c79426e 100644 --- a/service_test.go +++ b/service_test.go @@ -145,10 +145,6 @@ var mockTransactions = []Nip47Transaction{ } var mockTransaction = &mockTransactions[0] -func TestBackwardsCompatibility(t *testing.T) { - // Test without adding a single permission -} - // TODO: add tests for HandleEvent method as these // only cover the cases after the event is processed // and the corresponding app is found