From 9f46bb2d86e2e99ceca3b884653354a2e0a2a5f7 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Thu, 20 Jun 2024 23:14:29 +0700 Subject: [PATCH] Chore: payment sent notification improvements (#472) --- events/models.go | 5 -- frontend/src/components/EmptyState.tsx | 2 +- lnclient/ldk/ldk.go | 9 --- nip47/controllers/get_info_controller.go | 38 +++++++----- nip47/controllers/get_info_controller_test.go | 62 +++++++++++++++++++ nip47/notifications/models.go | 6 +- nip47/notifications/nip47_notifier.go | 37 +++++------ nip47/notifications/nip47_notifier_test.go | 7 --- nip47/permissions/permissions.go | 20 +++++- tests/mock_ln_client.go | 1 - 10 files changed, 128 insertions(+), 59 deletions(-) diff --git a/events/models.go b/events/models.go index 936e89b3c..22aef1c5b 100644 --- a/events/models.go +++ b/events/models.go @@ -20,15 +20,10 @@ type Event struct { type PaymentReceivedEventProperties struct { PaymentHash string `json:"payment_hash"` - Amount uint64 `json:"amount"` - NodeType string `json:"node_type"` } type PaymentSentEventProperties struct { - PaymentId string `json:"payment_id"` PaymentHash string `json:"payment_hash"` - Amount uint64 `json:"amount"` - NodeType string `json:"node_type"` } type ChannelBackupEvent struct { diff --git a/frontend/src/components/EmptyState.tsx b/frontend/src/components/EmptyState.tsx index 1ef6da43d..939a7a089 100644 --- a/frontend/src/components/EmptyState.tsx +++ b/frontend/src/components/EmptyState.tsx @@ -19,7 +19,7 @@ const EmptyState: React.FC = ({ buttonLink, }) => { return ( -
+

{message}

diff --git a/lnclient/ldk/ldk.go b/lnclient/ldk/ldk.go index ee820c0de..62c6bb42f 100644 --- a/lnclient/ldk/ldk.go +++ b/lnclient/ldk/ldk.go @@ -370,10 +370,6 @@ func (ls *LDKService) resetRouterInternal() { logger.Logger.WithFields(logrus.Fields{ "rowsAffected": rowsAffected, }).Info("Reset router") - - if err != nil { - logger.Logger.WithField("key", key).WithError(err).Error("ResetRouter failed") - } } } @@ -1225,18 +1221,13 @@ func (ls *LDKService) handleLdkEvent(event *ldk_node.Event) { Event: "nwc_payment_received", Properties: &events.PaymentReceivedEventProperties{ PaymentHash: eventType.PaymentHash, - Amount: eventType.AmountMsat / 1000, - NodeType: config.LDKBackendType, }, }) case ldk_node.EventPaymentSuccessful: ls.eventPublisher.Publish(&events.Event{ Event: "nwc_payment_sent", Properties: &events.PaymentSentEventProperties{ - PaymentId: *eventType.PaymentId, PaymentHash: eventType.PaymentHash, - Amount: *eventType.FeePaidMsat / 1000, - NodeType: config.LDKBackendType, }, }) } diff --git a/nip47/controllers/get_info_controller.go b/nip47/controllers/get_info_controller.go index add1a6808..2994d44fa 100644 --- a/nip47/controllers/get_info_controller.go +++ b/nip47/controllers/get_info_controller.go @@ -2,24 +2,27 @@ package controllers import ( "context" + "strings" "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/lnclient" "github.com/getAlby/nostr-wallet-connect/logger" "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/getAlby/nostr-wallet-connect/nip47/notifications" permissions "github.com/getAlby/nostr-wallet-connect/nip47/permissions" "github.com/nbd-wtf/go-nostr" "github.com/sirupsen/logrus" ) type getInfoResponse struct { - Alias string `json:"alias"` - Color string `json:"color"` - Pubkey string `json:"pubkey"` - Network string `json:"network"` - BlockHeight uint32 `json:"block_height"` - BlockHash string `json:"block_hash"` - Methods []string `json:"methods"` + Alias string `json:"alias"` + Color string `json:"color"` + Pubkey string `json:"pubkey"` + Network string `json:"network"` + BlockHeight uint32 `json:"block_height"` + BlockHash string `json:"block_hash"` + Methods []string `json:"methods"` + Notifications []string `json:"notifications"` } type getInfoController struct { @@ -68,14 +71,21 @@ func (controller *getInfoController) HandleGetInfoEvent(ctx context.Context, nip network = "mainnet" } + supportedNotifications := []string{} + if controller.permissionsService.PermitsNotifications(app) { + // TODO: this needs to be LNClient-specific + supportedNotifications = strings.Split(notifications.NOTIFICATION_TYPES, " ") + } + responsePayload := &getInfoResponse{ - Alias: info.Alias, - Color: info.Color, - Pubkey: info.Pubkey, - Network: network, - BlockHeight: info.BlockHeight, - BlockHash: info.BlockHash, - Methods: controller.permissionsService.GetPermittedMethods(app), + Alias: info.Alias, + Color: info.Color, + Pubkey: info.Pubkey, + Network: network, + BlockHeight: info.BlockHeight, + BlockHash: info.BlockHash, + Methods: controller.permissionsService.GetPermittedMethods(app), + Notifications: supportedNotifications, } publishResponse(&models.Response{ diff --git a/nip47/controllers/get_info_controller_test.go b/nip47/controllers/get_info_controller_test.go index 925a6ba13..79cbdee00 100644 --- a/nip47/controllers/get_info_controller_test.go +++ b/nip47/controllers/get_info_controller_test.go @@ -111,4 +111,66 @@ func TestHandleGetInfoEvent_WithPermission(t *testing.T) { assert.Equal(t, tests.MockNodeInfo.BlockHeight, nodeInfo.BlockHeight) assert.Equal(t, tests.MockNodeInfo.BlockHash, nodeInfo.BlockHash) assert.Equal(t, []string{"get_info"}, nodeInfo.Methods) + assert.Equal(t, []string{}, nodeInfo.Notifications) +} + +func TestHandleGetInfoEvent_WithNotifications(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47GetInfoJson), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + appPermission := &db.AppPermission{ + AppId: app.ID, + RequestMethod: models.GET_INFO_METHOD, + ExpiresAt: nil, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + // TODO: AppPermission RequestMethod needs to change to scope + appPermission = &db.AppPermission{ + AppId: app.ID, + RequestMethod: "notifications", + ExpiresAt: nil, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + checkPermission := func(amountMsat uint64) *models.Response { + return nil + } + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + + NewGetInfoController(permissionsSvc, svc.LNClient). + HandleGetInfoEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse) + + assert.Nil(t, publishedResponse.Error) + nodeInfo := publishedResponse.Result.(*getInfoResponse) + assert.Equal(t, tests.MockNodeInfo.Alias, nodeInfo.Alias) + assert.Equal(t, tests.MockNodeInfo.Color, nodeInfo.Color) + assert.Equal(t, tests.MockNodeInfo.Pubkey, nodeInfo.Pubkey) + assert.Equal(t, tests.MockNodeInfo.Network, nodeInfo.Network) + assert.Equal(t, tests.MockNodeInfo.BlockHeight, nodeInfo.BlockHeight) + assert.Equal(t, tests.MockNodeInfo.BlockHash, nodeInfo.BlockHash) + assert.Equal(t, []string{"get_info"}, nodeInfo.Methods) + assert.Equal(t, []string{"payment_received", "payment_sent"}, nodeInfo.Notifications) } diff --git a/nip47/notifications/models.go b/nip47/notifications/models.go index 540a137a3..fb6212fcc 100644 --- a/nip47/notifications/models.go +++ b/nip47/notifications/models.go @@ -8,11 +8,15 @@ type Notification struct { } const ( - NOTIFICATION_TYPES = "payment_received payment_sent" // e.g. "payment_received payment_sent balance_updated payment_sent channel_opened channel_closed ..." + NOTIFICATION_TYPES = "payment_received payment_sent" PAYMENT_RECEIVED_NOTIFICATION = "payment_received" PAYMENT_SENT_NOTIFICATION = "payment_sent" ) +type PaymentSentNotification struct { + models.Transaction +} + type PaymentReceivedNotification struct { models.Transaction } diff --git a/nip47/notifications/nip47_notifier.go b/nip47/notifications/nip47_notifier.go index 988c87673..a4cc492f3 100644 --- a/nip47/notifications/nip47_notifier.go +++ b/nip47/notifications/nip47_notifier.go @@ -44,12 +44,6 @@ func NewNip47Notifier(relay Relay, db *gorm.DB, cfg config.Config, keys keys.Key } func (notifier *Nip47Notifier) ConsumeEvent(ctx context.Context, event *events.Event) error { - if event.Event != "nwc_payment_received" && event.Event != "nwc_payment_sent" { - return nil - } - - var transaction interface{} - var notificationType string switch event.Event { case "nwc_payment_received": paymentReceivedEventProperties, ok := event.Properties.(*events.PaymentReceivedEventProperties) @@ -58,8 +52,7 @@ func (notifier *Nip47Notifier) ConsumeEvent(ctx context.Context, event *events.E return errors.New("failed to cast event") } - var err error - transaction, err = notifier.lnClient.LookupInvoice(ctx, paymentReceivedEventProperties.PaymentHash) + transaction, err := notifier.lnClient.LookupInvoice(ctx, paymentReceivedEventProperties.PaymentHash) if err != nil { logger.Logger. WithField("paymentHash", paymentReceivedEventProperties.PaymentHash). @@ -67,7 +60,15 @@ func (notifier *Nip47Notifier) ConsumeEvent(ctx context.Context, event *events.E Error("Failed to lookup invoice by payment hash") return err } - notificationType = PAYMENT_RECEIVED_NOTIFICATION + notification := PaymentReceivedNotification{ + Transaction: *transaction, + } + + notifier.notifySubscribers(ctx, &Notification{ + Notification: notification, + NotificationType: PAYMENT_RECEIVED_NOTIFICATION, + }, nostr.Tags{}) + case "nwc_payment_sent": paymentSentEventProperties, ok := event.Properties.(*events.PaymentSentEventProperties) if !ok { @@ -75,8 +76,7 @@ func (notifier *Nip47Notifier) ConsumeEvent(ctx context.Context, event *events.E return errors.New("failed to cast event") } - var err error - transaction, err = notifier.lnClient.LookupInvoice(ctx, paymentSentEventProperties.PaymentHash) + transaction, err := notifier.lnClient.LookupInvoice(ctx, paymentSentEventProperties.PaymentHash) if err != nil { logger.Logger. WithField("paymentHash", paymentSentEventProperties.PaymentHash). @@ -84,15 +84,16 @@ func (notifier *Nip47Notifier) ConsumeEvent(ctx context.Context, event *events.E Error("Failed to lookup invoice by payment hash") return err } - notificationType = PAYMENT_SENT_NOTIFICATION - default: - logger.Logger.Fatalf("Unsupported notification type: %v", event.Event) + notification := PaymentSentNotification{ + Transaction: *transaction, + } + + notifier.notifySubscribers(ctx, &Notification{ + Notification: notification, + NotificationType: PAYMENT_SENT_NOTIFICATION, + }, nostr.Tags{}) } - notifier.notifySubscribers(ctx, &Notification{ - Notification: transaction, - NotificationType: notificationType, - }, nostr.Tags{}) return nil } diff --git a/nip47/notifications/nip47_notifier_test.go b/nip47/notifications/nip47_notifier_test.go index 0bc2d5f99..375d26e77 100644 --- a/nip47/notifications/nip47_notifier_test.go +++ b/nip47/notifications/nip47_notifier_test.go @@ -44,8 +44,6 @@ func TestSendNotification(t *testing.T) { Event: "nwc_payment_received", Properties: &events.PaymentReceivedEventProperties{ PaymentHash: tests.MockPaymentHash, - Amount: uint64(tests.MockTransaction.Amount), - NodeType: "LDK", }, } @@ -88,10 +86,7 @@ func TestSendNotification(t *testing.T) { testEvent = &events.Event{ Event: "nwc_payment_sent", Properties: &events.PaymentSentEventProperties{ - PaymentId: tests.MockPaymentId, PaymentHash: tests.MockPaymentHash, - Amount: uint64(tests.MockTransaction.Amount), - NodeType: "LDK", }, } @@ -121,8 +116,6 @@ func TestSendNotificationNoPermission(t *testing.T) { Event: "nwc_payment_received", Properties: &events.PaymentReceivedEventProperties{ PaymentHash: tests.MockPaymentHash, - Amount: uint64(tests.MockTransaction.Amount), - NodeType: "LDK", }, } diff --git a/nip47/permissions/permissions.go b/nip47/permissions/permissions.go index 2f26b5f4a..0278d948b 100644 --- a/nip47/permissions/permissions.go +++ b/nip47/permissions/permissions.go @@ -28,6 +28,7 @@ type PermissionsService interface { HasPermission(app *db.App, requestMethod string, amount uint64) (result bool, code string, message string) GetBudgetUsage(appPermission *db.AppPermission) uint64 GetPermittedMethods(app *db.App) []string + PermitsNotifications(app *db.App) bool } func NewPermissionsService(db *gorm.DB, eventPublisher events.EventPublisher) *permissionsService { @@ -88,9 +89,9 @@ func (svc *permissionsService) GetBudgetUsage(appPermission *db.AppPermission) u func (svc *permissionsService) GetPermittedMethods(app *db.App) []string { appPermissions := []db.AppPermission{} - svc.db.Find(&appPermissions, &db.AppPermission{ - AppId: app.ID, - }) + // TODO: request_method needs to be renamed to scopes or capabilities + // see https://github.com/getAlby/nostr-wallet-connect-next/issues/219 + svc.db.Where("app_id = ? and request_method <> ?", app.ID, "notifications").Find(&appPermissions) requestMethods := make([]string, 0, len(appPermissions)) for _, appPermission := range appPermissions { requestMethods = append(requestMethods, appPermission.RequestMethod) @@ -103,6 +104,19 @@ func (svc *permissionsService) GetPermittedMethods(app *db.App) []string { return requestMethods } +func (svc *permissionsService) PermitsNotifications(app *db.App) bool { + notificationPermission := db.AppPermission{} + err := svc.db.First(¬ificationPermission, &db.AppPermission{ + AppId: app.ID, + RequestMethod: "notifications", + }).Error + if err != nil { + return false + } + + return true +} + func getStartOfBudget(budget_type string) time.Time { now := time.Now() switch budget_type { diff --git a/tests/mock_ln_client.go b/tests/mock_ln_client.go index 34f1449f8..8dc833f0e 100644 --- a/tests/mock_ln_client.go +++ b/tests/mock_ln_client.go @@ -13,7 +13,6 @@ const MockPaymentHash500 = "be8ad5d0b82071d538dcd160e3a3af444bd890de68388a4d771b const MockInvoice = "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" const MockPaymentHash = "320c2c5a1492ccfd5bc7aa4ad9b657d6aaec3cfcc0d1d98413a29af4ac772ccf" // for the above invoice -const MockPaymentId = "paymentId123" var MockNodeInfo = lnclient.NodeInfo{ Alias: "bob",