Skip to content
This repository has been archived by the owner on Sep 2, 2024. It is now read-only.

feat: add payment_sent notification #436

Merged
merged 5 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions events/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/EmptyState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const EmptyState: React.FC<Props> = ({
buttonLink,
}) => {
return (
<div className="flex flex-1 items-center justify-center rounded-lg border border-dashed shadow-sm">
<div className="flex flex-1 items-center justify-center rounded-lg border border-dashed shadow-sm py-8">
<div className="flex flex-col items-center gap-1 text-center max-w-sm">
<Icon className="w-10 h-10 text-muted-foreground" />
<h3 className="mt-4 text-lg font-semibold">{message}</h3>
Expand Down
13 changes: 7 additions & 6 deletions lnclient/ldk/ldk.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
}

Expand Down Expand Up @@ -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,
},
})
}
Expand Down
38 changes: 24 additions & 14 deletions nip47/controllers/get_info_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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{
Expand Down
62 changes: 62 additions & 0 deletions nip47/controllers/get_info_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
7 changes: 6 additions & 1 deletion nip47/notifications/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
65 changes: 46 additions & 19 deletions nip47/notifications/nip47_notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
75 changes: 65 additions & 10 deletions nip47/notifications/nip47_notifier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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",
},
}

Expand Down Expand Up @@ -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)

rolznz marked this conversation as resolved.
Show resolved Hide resolved
}
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)
Expand Down Expand Up @@ -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",
},
}

Expand Down
Loading
Loading