diff --git a/.github/workflows/wails2.yaml b/.github/workflows/wails.yml similarity index 100% rename from .github/workflows/wails2.yaml rename to .github/workflows/wails.yml diff --git a/events/models.go b/events/models.go index bd498796..22aef1c5 100644 --- a/events/models.go +++ b/events/models.go @@ -20,8 +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 { + PaymentHash string `json:"payment_hash"` } type ChannelBackupEvent struct { diff --git a/frontend/src/components/EmptyState.tsx b/frontend/src/components/EmptyState.tsx index 1ef6da43..939a7a08 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/frontend/src/components/SidebarHint.tsx b/frontend/src/components/SidebarHint.tsx index 370b1945..b7d5e2b7 100644 --- a/frontend/src/components/SidebarHint.tsx +++ b/frontend/src/components/SidebarHint.tsx @@ -82,7 +82,8 @@ function SidebarHint() { if ( albyMe && nodeConnectionInfo && - albyMe?.keysend_pubkey !== nodeConnectionInfo?.pubkey + albyMe?.keysend_pubkey !== nodeConnectionInfo?.pubkey && + !location.pathname.startsWith("/apps") ) { return (
-
- - - - {connection && } -
+ {connection && ( +
+ + + + +
+ )}
diff --git a/frontend/src/hooks/useLinkAccount.ts b/frontend/src/hooks/useLinkAccount.ts index 6d66a853..9a858982 100644 --- a/frontend/src/hooks/useLinkAccount.ts +++ b/frontend/src/hooks/useLinkAccount.ts @@ -1,6 +1,7 @@ import { useState } from "react"; import { toast } from "src/components/ui/use-toast"; import { useAlbyMe } from "src/hooks/useAlbyMe"; +import { useApps } from "src/hooks/useApps"; import { useCSRF } from "src/hooks/useCSRF"; import { useNodeConnectionInfo } from "src/hooks/useNodeConnectionInfo"; import { request } from "src/utils/request"; @@ -14,6 +15,7 @@ export enum LinkStatus { export function useLinkAccount() { const { data: csrf } = useCSRF(); const { data: me, mutate: reloadAlbyMe } = useAlbyMe(); + const { mutate: reloadApps } = useApps(); const { data: nodeConnectionInfo } = useNodeConnectionInfo(); const [loading, setLoading] = useState(false); @@ -43,7 +45,8 @@ export function useLinkAccount() { "Content-Type": "application/json", }, }); - await reloadAlbyMe(); + // update the link status and get the newly-created Alby Account app + await Promise.all([reloadAlbyMe(), reloadApps()]); toast({ title: "Your Alby Hub has successfully been linked to your Alby Account", diff --git a/lnclient/ldk/ldk.go b/lnclient/ldk/ldk.go index 714d512d..62c6bb42 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,8 +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{ + PaymentHash: eventType.PaymentHash, }, }) } diff --git a/nip47/controllers/get_info_controller.go b/nip47/controllers/get_info_controller.go index add1a680..2994d44f 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 925a6ba1..79cbdee0 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 e981dc60..fb6212fc 100644 --- a/nip47/notifications/models.go +++ b/nip47/notifications/models.go @@ -8,10 +8,15 @@ type Notification struct { } const ( - NOTIFICATION_TYPES = "payment_received" // 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 da8eca92..a4cc492f 100644 --- a/nip47/notifications/nip47_notifier.go +++ b/nip47/notifications/nip47_notifier.go @@ -44,29 +44,56 @@ 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" { - return nil - } + switch event.Event { + case "nwc_payment_received": + paymentReceivedEventProperties, ok := event.Properties.(*events.PaymentReceivedEventProperties) + if !ok { + logger.Logger.WithField("event", event).Error("Failed to cast event") + return errors.New("failed to cast event") + } - paymentReceivedEventProperties, ok := event.Properties.(*events.PaymentReceivedEventProperties) - if !ok { - logger.Logger.WithField("event", event).Error("Failed to cast event") - return errors.New("failed to cast event") - } + transaction, err := notifier.lnClient.LookupInvoice(ctx, paymentReceivedEventProperties.PaymentHash) + if err != nil { + logger.Logger. + WithField("paymentHash", paymentReceivedEventProperties.PaymentHash). + WithError(err). + Error("Failed to lookup invoice by payment hash") + return err + } + notification := PaymentReceivedNotification{ + Transaction: *transaction, + } - transaction, err := notifier.lnClient.LookupInvoice(ctx, paymentReceivedEventProperties.PaymentHash) - if err != nil { - logger.Logger. - WithField("paymentHash", paymentReceivedEventProperties.PaymentHash). - WithError(err). - Error("Failed to lookup invoice by payment hash") - return err + notifier.notifySubscribers(ctx, &Notification{ + Notification: notification, + NotificationType: PAYMENT_RECEIVED_NOTIFICATION, + }, nostr.Tags{}) + + case "nwc_payment_sent": + paymentSentEventProperties, ok := event.Properties.(*events.PaymentSentEventProperties) + if !ok { + logger.Logger.WithField("event", event).Error("Failed to cast event") + return errors.New("failed to cast event") + } + + transaction, err := notifier.lnClient.LookupInvoice(ctx, paymentSentEventProperties.PaymentHash) + if err != nil { + logger.Logger. + WithField("paymentHash", paymentSentEventProperties.PaymentHash). + WithError(err). + Error("Failed to lookup invoice by payment hash") + return err + } + notification := PaymentSentNotification{ + Transaction: *transaction, + } + + notifier.notifySubscribers(ctx, &Notification{ + Notification: notification, + NotificationType: PAYMENT_SENT_NOTIFICATION, + }, nostr.Tags{}) } - notifier.notifySubscribers(ctx, &Notification{ - Notification: transaction, - NotificationType: PAYMENT_RECEIVED_NOTIFICATION, - }, nostr.Tags{}) return nil } diff --git a/nip47/notifications/nip47_notifier_test.go b/nip47/notifications/nip47_notifier_test.go index efb45a17..64e3d6cb 100644 --- a/nip47/notifications/nip47_notifier_test.go +++ b/nip47/notifications/nip47_notifier_test.go @@ -15,17 +15,12 @@ import ( "github.com/stretchr/testify/assert" ) -func TestSendNotification(t *testing.T) { +func TestSendNotification_PaymentReceived(t *testing.T) { ctx := context.TODO() defer tests.RemoveTestService() svc, err := tests.CreateTestService() assert.NoError(t, err) - /*mockLn, err := NewMockLn() - assert.NoError(t, err) - svc, err := createTestService(mockLn) - assert.NoError(t, err)*/ - app, ss, err := tests.CreateApp(svc) assert.NoError(t, err) @@ -44,8 +39,6 @@ func TestSendNotification(t *testing.T) { Event: "nwc_payment_received", Properties: &events.PaymentReceivedEventProperties{ PaymentHash: tests.MockPaymentHash, - Amount: uint64(tests.MockTransaction.Amount), - NodeType: "LDK", }, } @@ -74,6 +67,70 @@ func TestSendNotification(t *testing.T) { assert.NoError(t, err) assert.Equal(t, PAYMENT_RECEIVED_NOTIFICATION, unmarshalledResponse.NotificationType) + transaction := (unmarshalledResponse.Notification.(*PaymentReceivedNotification)) + assert.Equal(t, tests.MockTransaction.Type, transaction.Type) + assert.Equal(t, tests.MockTransaction.Invoice, transaction.Invoice) + assert.Equal(t, tests.MockTransaction.Description, transaction.Description) + assert.Equal(t, tests.MockTransaction.DescriptionHash, transaction.DescriptionHash) + assert.Equal(t, tests.MockTransaction.Preimage, transaction.Preimage) + assert.Equal(t, tests.MockTransaction.PaymentHash, transaction.PaymentHash) + assert.Equal(t, tests.MockTransaction.Amount, transaction.Amount) + assert.Equal(t, tests.MockTransaction.FeesPaid, transaction.FeesPaid) + assert.Equal(t, tests.MockTransaction.SettledAt, transaction.SettledAt) + +} +func TestSendNotification_PaymentSent(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, ss, err := tests.CreateApp(svc) + assert.NoError(t, err) + + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + RequestMethod: permissions.NOTIFICATIONS_PERMISSION, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + nip47NotificationQueue := NewNip47NotificationQueue() + svc.EventPublisher.RegisterSubscriber(nip47NotificationQueue) + + testEvent := &events.Event{ + Event: "nwc_payment_sent", + Properties: &events.PaymentSentEventProperties{ + PaymentHash: tests.MockPaymentHash, + }, + } + + svc.EventPublisher.Publish(testEvent) + + receivedEvent := <-nip47NotificationQueue.Channel() + assert.Equal(t, testEvent, receivedEvent) + + relay := NewMockRelay() + + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + + notifier := NewNip47Notifier(relay, svc.DB, svc.Cfg, svc.Keys, permissionsSvc, svc.LNClient) + notifier.ConsumeEvent(ctx, receivedEvent) + + assert.NotNil(t, relay.publishedEvent) + assert.NotEmpty(t, relay.publishedEvent.Content) + + decrypted, err := nip04.Decrypt(relay.publishedEvent.Content, ss) + assert.NoError(t, err) + unmarshalledResponse := Notification{ + Notification: &PaymentReceivedNotification{}, + } + + err = json.Unmarshal([]byte(decrypted), &unmarshalledResponse) + assert.NoError(t, err) + assert.Equal(t, PAYMENT_SENT_NOTIFICATION, unmarshalledResponse.NotificationType) + transaction := (unmarshalledResponse.Notification.(*PaymentReceivedNotification)) assert.Equal(t, tests.MockTransaction.Type, transaction.Type) assert.Equal(t, tests.MockTransaction.Invoice, transaction.Invoice) @@ -101,8 +158,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 2f26b5f4..0278d948 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 {