event_handler.go raw
1 package nip47
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "slices"
9 "time"
10
11 "github.com/getAlby/go-nostr"
12 "github.com/getAlby/hub/constants"
13 "github.com/getAlby/hub/db"
14 "github.com/getAlby/hub/events"
15 "github.com/getAlby/hub/lnclient"
16 "github.com/getAlby/hub/logger"
17 "github.com/getAlby/hub/nip47/cipher"
18 "github.com/getAlby/hub/nip47/controllers"
19 "github.com/getAlby/hub/nip47/models"
20 "github.com/getAlby/hub/nip47/permissions"
21 nostrmodels "github.com/getAlby/hub/nostr/models"
22 "github.com/sirupsen/logrus"
23 "gorm.io/gorm"
24 )
25
26 func (svc *nip47Service) HandleEvent(ctx context.Context, pool nostrmodels.SimplePool, event *nostr.Event, lnClient lnclient.LNClient) {
27 var nip47Response *models.Response
28 logger.Logger.WithFields(logrus.Fields{
29 "requestEventNostrId": event.ID,
30 "eventKind": event.Kind,
31 }).Info("Processing Event")
32
33 // go-nostr already checks this, but just to be sure:
34 validEventSignature, err := event.CheckSignature()
35 if err != nil {
36 logger.Logger.WithFields(logrus.Fields{
37 "requestEventNostrId": event.ID,
38 "eventKind": event.Kind,
39 }).WithError(err).Error("invalid event signature")
40 return
41 }
42 if !validEventSignature {
43 logger.Logger.WithFields(logrus.Fields{
44 "requestEventNostrId": event.ID,
45 "eventKind": event.Kind,
46 }).Error("invalid event signature")
47 return
48 }
49
50 if lnClient == nil {
51 logger.Logger.WithFields(logrus.Fields{
52 "requestEventNostrId": event.ID,
53 }).Error("cannot handle event due to LNClient not started")
54 return
55 }
56
57 // store request event
58 requestEvent := db.RequestEvent{AppId: nil, NostrId: event.ID, State: db.REQUEST_EVENT_STATE_HANDLER_EXECUTING}
59 err = svc.db.Create(&requestEvent).Error
60 if err != nil {
61 if errors.Is(err, gorm.ErrDuplicatedKey) {
62 logger.Logger.WithFields(logrus.Fields{
63 "requestEventNostrId": event.ID,
64 }).Warn("Event already processed")
65 return
66 }
67 logger.Logger.WithFields(logrus.Fields{
68 "requestEventNostrId": event.ID,
69 "eventKind": event.Kind,
70 }).WithError(err).Error("Failed to save nostr event")
71 return
72 }
73 app := db.App{}
74 err = svc.db.First(&app, &db.App{
75 AppPubkey: event.PubKey,
76 }).Error
77 if err != nil {
78 logger.Logger.WithFields(logrus.Fields{
79 "appPubkey": event.PubKey,
80 }).WithError(err).Error("Failed to find app for nostr pubkey")
81 return
82 }
83
84 now := time.Now()
85 err = svc.db.Model(&app).Update("last_used_at", &now).Error
86 if err != nil {
87 logger.Logger.WithFields(logrus.Fields{
88 "app_id": app.ID,
89 }).WithError(err).Error("Failed to update app last used time")
90 }
91
92 logger.Logger.WithFields(logrus.Fields{
93 "requestEventNostrId": event.ID,
94 "eventKind": event.Kind,
95 "appId": app.ID,
96 }).Debug("App found for nostr event")
97
98 appWalletPrivKey := svc.keys.GetNostrSecretKey()
99
100 if app.WalletPubkey != nil {
101 // This is a new child key derived from master using app ID as index
102 appWalletPrivKey, err = svc.keys.GetAppWalletKey(app.ID)
103 if err != nil {
104 logger.Logger.WithFields(logrus.Fields{
105 "appId": app.ID,
106 }).WithError(err).Error("error deriving child key")
107 return
108 }
109 }
110
111 encryption := constants.ENCRYPTION_TYPE_NIP04
112 encryptionTag := event.Tags.Find("encryption")
113 if encryptionTag != nil {
114 encryption = encryptionTag[1]
115 }
116
117 nip47Cipher, err := cipher.NewNip47Cipher(encryption, app.AppPubkey, appWalletPrivKey)
118 if err != nil {
119 cipherErr := err
120 logger.Logger.WithFields(logrus.Fields{
121 "requestEventNostrId": event.ID,
122 "eventKind": event.Kind,
123 "appId": app.ID,
124 "encryption": encryption,
125 }).WithError(err).Error("Failed to initialize cipher")
126
127 err = svc.db.
128 Model(&requestEvent).
129 Update("state", db.REQUEST_EVENT_STATE_HANDLER_ERROR).
130 Error
131 if err != nil {
132 logger.Logger.WithFields(logrus.Fields{
133 "appPubkey": event.PubKey,
134 }).WithError(err).Error("Failed to save state to nostr event")
135 }
136
137 // whenever we are unable to handle the request encryption, we always respond with our preferred encryption
138 // re-create the cipher with NIP-44 to send an error response
139 nip47Cipher, err := cipher.NewNip47Cipher(constants.ENCRYPTION_TYPE_NIP44_V2, app.AppPubkey, appWalletPrivKey)
140
141 if err != nil {
142 logger.Logger.WithFields(logrus.Fields{
143 "requestEventNostrId": event.ID,
144 "eventKind": event.Kind,
145 "appId": app.ID,
146 "encryption": encryption,
147 }).WithError(err).Error("Failed to initialize cipher")
148 return
149 }
150
151 nip47Response = &models.Response{
152 Error: &models.Error{
153 Code: constants.ERROR_UNSUPPORTED_ENCRYPTION,
154 Message: cipherErr.Error(),
155 },
156 }
157
158 resp, err := svc.CreateResponse(event, nip47Response, nostr.Tags{}, nip47Cipher, appWalletPrivKey)
159 if err != nil {
160 logger.Logger.WithFields(logrus.Fields{
161 "requestEventNostrId": event.ID,
162 "eventKind": event.Kind,
163 }).WithError(err).Error("Failed to process event")
164 }
165 svc.publishResponseEvent(ctx, pool, &requestEvent, resp, &app)
166
167 return
168 }
169
170 err = svc.db.
171 Model(&requestEvent).
172 Update("app_id", app.ID).
173 Error
174 if err != nil {
175 logger.Logger.WithFields(logrus.Fields{
176 "appPubkey": event.PubKey,
177 }).WithError(err).Error("Failed to save app to nostr event")
178
179 nip47Response = &models.Response{
180 Error: &models.Error{
181 Code: constants.ERROR_INTERNAL,
182 Message: fmt.Sprintf("Failed to save app to nostr event: %s", err.Error()),
183 },
184 }
185 resp, err := svc.CreateResponse(event, nip47Response, nostr.Tags{}, nip47Cipher, appWalletPrivKey)
186 if err != nil {
187 logger.Logger.WithFields(logrus.Fields{
188 "requestEventNostrId": event.ID,
189 "eventKind": event.Kind,
190 }).WithError(err).Error("Failed to process event")
191 }
192 svc.publishResponseEvent(ctx, pool, &requestEvent, resp, &app)
193
194 err = svc.db.
195 Model(&requestEvent).
196 Update("state", db.REQUEST_EVENT_STATE_HANDLER_ERROR).
197 Error
198 if err != nil {
199 logger.Logger.WithFields(logrus.Fields{
200 "appPubkey": event.PubKey,
201 }).WithError(err).Error("Failed to save state to nostr event")
202 }
203
204 return
205 }
206
207 payload, err := nip47Cipher.Decrypt(event.Content)
208 if err != nil {
209 decryptionErr := err
210 logger.Logger.WithFields(logrus.Fields{
211 "requestEventNostrId": event.ID,
212 "eventKind": event.Kind,
213 "appId": app.ID,
214 }).WithError(err).Error("Failed to decrypt content")
215
216 err = svc.db.
217 Model(&requestEvent).
218 Update("state", db.REQUEST_EVENT_STATE_HANDLER_ERROR).
219 Error
220 if err != nil {
221 logger.Logger.WithFields(logrus.Fields{
222 "appPubkey": event.PubKey,
223 }).WithError(err).Error("Failed to save state to nostr event")
224 }
225
226 // whenever we are unable to handle the request encryption, we always respond with our preferred encryption
227 // re-create the cipher with NIP-44 to send an error response
228 nip47Cipher, err := cipher.NewNip47Cipher(constants.ENCRYPTION_TYPE_NIP44_V2, app.AppPubkey, appWalletPrivKey)
229
230 if err != nil {
231 logger.Logger.WithFields(logrus.Fields{
232 "requestEventNostrId": event.ID,
233 "eventKind": event.Kind,
234 "appId": app.ID,
235 "encryption": encryption,
236 }).WithError(err).Error("Failed to initialize cipher")
237 return
238 }
239
240 nip47Response = &models.Response{
241 Error: &models.Error{
242 Code: constants.ERROR_BAD_REQUEST,
243 Message: fmt.Sprintf("failed to decrypt: %s", decryptionErr.Error()),
244 },
245 }
246
247 resp, err := svc.CreateResponse(event, nip47Response, nostr.Tags{}, nip47Cipher, appWalletPrivKey)
248 if err != nil {
249 logger.Logger.WithFields(logrus.Fields{
250 "requestEventNostrId": event.ID,
251 "eventKind": event.Kind,
252 }).WithError(err).Error("Failed to process event")
253 }
254 svc.publishResponseEvent(ctx, pool, &requestEvent, resp, &app)
255
256 return
257 }
258 nip47Request := &models.Request{}
259 err = json.Unmarshal([]byte(payload), nip47Request)
260 if err != nil {
261 logger.Logger.WithFields(logrus.Fields{
262 "requestEventNostrId": event.ID,
263 "eventKind": event.Kind,
264 }).WithError(err).Error("Failed to process event")
265
266 err = svc.db.
267 Model(&requestEvent).
268 Update("state", db.REQUEST_EVENT_STATE_HANDLER_ERROR).
269 Error
270 if err != nil {
271 logger.Logger.WithFields(logrus.Fields{
272 "appPubkey": event.PubKey,
273 }).WithError(err).Error("Failed to save state to nostr event")
274 }
275
276 return
277 }
278
279 // we ignore potential DB errors here as this only saves the method and content data
280 svc.db.Model(&requestEvent).Updates(map[string]interface{}{
281 "method": nip47Request.Method,
282 "content_data": payload,
283 })
284 // TODO: replace with a channel
285 // TODO: update all previous occurrences of svc.publishResponseEvent to also use the channel
286 publishResponse := func(nip47Response *models.Response, tags nostr.Tags) {
287 var state string
288 resp, err := svc.CreateResponse(event, nip47Response, tags, nip47Cipher, appWalletPrivKey)
289 if err != nil {
290 logger.Logger.WithFields(logrus.Fields{
291 "requestEventNostrId": event.ID,
292 "eventKind": event.Kind,
293 "appId": app.ID,
294 }).WithError(err).Error("Failed to create response")
295 state = db.REQUEST_EVENT_STATE_HANDLER_ERROR
296 } else {
297 err = svc.publishResponseEvent(ctx, pool, &requestEvent, resp, &app)
298 if err != nil {
299 logger.Logger.WithFields(logrus.Fields{
300 "requestEventNostrId": event.ID,
301 "responseEventNostrId": resp.ID,
302 "eventKind": event.Kind,
303 "appId": app.ID,
304 }).WithError(err).Error("Failed to publish event")
305 state = db.REQUEST_EVENT_STATE_HANDLER_ERROR
306 } else {
307 logger.Logger.WithFields(logrus.Fields{
308 "requestEventNostrId": event.ID,
309 "responseEventNostrId": resp.ID,
310 "eventKind": event.Kind,
311 "appId": app.ID,
312 }).Debug("Published response")
313 state = db.REQUEST_EVENT_STATE_HANDLER_EXECUTED
314 }
315 }
316 err = svc.db.
317 Model(&requestEvent).
318 Update("state", state).
319 Error
320 if err != nil {
321 logger.Logger.WithFields(logrus.Fields{
322 "appPubkey": event.PubKey,
323 }).WithError(err).Error("Failed to save state to nostr event")
324 }
325 }
326
327 logger.Logger.WithFields(logrus.Fields{
328 "requestEventNostrId": event.ID,
329 "eventKind": event.Kind,
330 "appId": app.ID,
331 "method": nip47Request.Method,
332 "params": nip47Request.Params,
333 }).Debug("Handling NIP-47 request")
334
335 if !slices.Contains(permissions.GetAlwaysGrantedMethods(), nip47Request.Method) {
336 scope, err := permissions.RequestMethodToScope(nip47Request.Method)
337 if err != nil {
338 publishResponse(&models.Response{
339 ResultType: nip47Request.Method,
340 Error: &models.Error{
341 Code: constants.ERROR_INTERNAL,
342 Message: err.Error(),
343 },
344 }, nostr.Tags{})
345 return
346 }
347
348 // The relay could forward old requests, which is fine and actually also intended
349 // as it makes sure we can respond even after a downtime or network issue.
350 // but we should check the creation date of a request and ignore too old requests
351 // for payments and invoice creation.
352 if (scope == constants.PAY_INVOICE_SCOPE || scope == constants.MAKE_INVOICE_SCOPE) && time.Since(event.CreatedAt.Time()).Hours() > 6 {
353 logger.Logger.WithFields(logrus.Fields{
354 "request_event_id": requestEvent.ID,
355 "app_id": app.ID,
356 }).Error("Received request more than 6 hours old")
357
358 // ignore the request
359 return
360 }
361
362 hasPermission, code, message := svc.permissionsService.HasPermission(&app, scope)
363 if !hasPermission {
364 logger.Logger.WithFields(logrus.Fields{
365 "request_event_id": requestEvent.ID,
366 "app_id": app.ID,
367 "code": code,
368 "message": message,
369 }).Error("App does not have permission")
370
371 svc.eventPublisher.Publish(&events.Event{
372 Event: "nwc_permission_denied",
373 Properties: map[string]interface{}{
374 "request_method": nip47Request.Method,
375 "app_name": app.Name,
376 // "app_pubkey": app.AppPubkey,
377 "code": code,
378 "message": message,
379 },
380 })
381
382 publishResponse(&models.Response{
383 ResultType: nip47Request.Method,
384 Error: &models.Error{
385 Code: code,
386 Message: message,
387 },
388 }, nostr.Tags{})
389 return
390 }
391 }
392
393 controller := controllers.NewNip47Controller(lnClient, svc.db, svc.eventPublisher, svc.permissionsService, svc.transactionsService, svc.appsService, svc.albyOAuthSvc)
394
395 switch nip47Request.Method {
396 case models.MULTI_PAY_INVOICE_METHOD:
397 controller.
398 HandleMultiPayInvoiceEvent(ctx, nip47Request, requestEvent.ID, &app, publishResponse)
399 case models.MULTI_PAY_KEYSEND_METHOD:
400 controller.
401 HandleMultiPayKeysendEvent(ctx, nip47Request, requestEvent.ID, &app, publishResponse)
402 case models.PAY_INVOICE_METHOD:
403 controller.
404 HandlePayInvoiceEvent(ctx, nip47Request, requestEvent.ID, &app, publishResponse, nostr.Tags{})
405 case models.PAY_KEYSEND_METHOD:
406 controller.
407 HandlePayKeysendEvent(ctx, nip47Request, requestEvent.ID, &app, publishResponse, nostr.Tags{})
408 case models.GET_BALANCE_METHOD:
409 controller.
410 HandleGetBalanceEvent(ctx, nip47Request, requestEvent.ID, &app, publishResponse)
411 case models.GET_BUDGET_METHOD:
412 controller.
413 HandleGetBudgetEvent(ctx, nip47Request, requestEvent.ID, &app, publishResponse)
414 case models.MAKE_INVOICE_METHOD:
415 controller.
416 HandleMakeInvoiceEvent(ctx, nip47Request, requestEvent.ID, app.ID, publishResponse)
417 case models.LOOKUP_INVOICE_METHOD:
418 controller.
419 HandleLookupInvoiceEvent(ctx, nip47Request, requestEvent.ID, app.ID, publishResponse)
420 case models.LIST_TRANSACTIONS_METHOD:
421 controller.
422 HandleListTransactionsEvent(ctx, nip47Request, requestEvent.ID, app.ID, publishResponse)
423 case models.GET_INFO_METHOD:
424 controller.
425 HandleGetInfoEvent(ctx, nip47Request, requestEvent.ID, &app, publishResponse)
426 case models.SIGN_MESSAGE_METHOD:
427 controller.
428 HandleSignMessageEvent(ctx, nip47Request, requestEvent.ID, publishResponse)
429 case models.CREATE_CONNECTION_METHOD:
430 controller.
431 HandleCreateConnectionEvent(ctx, nip47Request, requestEvent.ID, publishResponse)
432 case models.MAKE_HOLD_INVOICE_METHOD:
433 controller.
434 HandleMakeHoldInvoiceEvent(ctx, nip47Request, requestEvent.ID, app.ID, publishResponse)
435 case models.CANCEL_HOLD_INVOICE_METHOD:
436 controller.
437 HandleCancelHoldInvoiceEvent(ctx, nip47Request, requestEvent.ID, app.ID, publishResponse)
438 case models.SETTLE_HOLD_INVOICE_METHOD:
439 controller.
440 HandleSettleHoldInvoiceEvent(ctx, nip47Request, requestEvent.ID, app.ID, publishResponse)
441 default:
442 publishResponse(&models.Response{
443 ResultType: nip47Request.Method,
444 Error: &models.Error{
445 Code: constants.ERROR_NOT_IMPLEMENTED,
446 Message: fmt.Sprintf("Unknown method: %s", nip47Request.Method),
447 },
448 }, nostr.Tags{})
449 }
450 }
451
452 func (svc *nip47Service) CreateResponse(initialEvent *nostr.Event, content interface{}, tags nostr.Tags, cipher *cipher.Nip47Cipher, appWalletPrivKey string) (result *nostr.Event, err error) {
453 payloadBytes, err := json.Marshal(content)
454 if err != nil {
455 return nil, err
456 }
457
458 msg, err := cipher.Encrypt(string(payloadBytes))
459 if err != nil {
460 return nil, err
461 }
462
463 allTags := nostr.Tags{[]string{"p", initialEvent.PubKey}, []string{"e", initialEvent.ID}}
464 allTags = append(allTags, tags...)
465
466 appWalletPubKey, err := nostr.GetPublicKey(appWalletPrivKey)
467 if err != nil {
468 logger.Logger.WithError(err).Error("Error converting nostr privkey to pubkey")
469 return
470 }
471
472 resp := &nostr.Event{
473 PubKey: appWalletPubKey,
474 CreatedAt: nostr.Now(),
475 Kind: models.RESPONSE_KIND,
476 Tags: allTags,
477 Content: msg,
478 }
479 err = resp.Sign(appWalletPrivKey)
480 if err != nil {
481 return nil, err
482 }
483 return resp, nil
484 }
485
486 func (svc *nip47Service) publishResponseEvent(ctx context.Context, pool nostrmodels.SimplePool, requestEvent *db.RequestEvent, resp *nostr.Event, app *db.App) error {
487 var appId *uint
488 if app != nil {
489 appId = &app.ID
490 }
491 responseEvent := db.ResponseEvent{NostrId: resp.ID, RequestId: requestEvent.ID, State: "received"}
492 err := svc.db.Create(&responseEvent).Error
493 if err != nil {
494 logger.Logger.WithFields(logrus.Fields{
495 "requestEventNostrId": requestEvent.NostrId,
496 "appId": appId,
497 "replyEventId": resp.ID,
498 }).WithError(err).Error("Failed to save response/reply event")
499 return err
500 }
501
502 updateColumns := make(map[string]interface{})
503 publishResultChannel := pool.PublishMany(ctx, svc.cfg.GetRelayUrls(), *resp)
504
505 publishSuccessful := false
506 for result := range publishResultChannel {
507 if result.Error == nil {
508 publishSuccessful = true
509 } else {
510 logger.Logger.WithFields(logrus.Fields{
511 "requestEventId": requestEvent.ID,
512 "requestNostrEventId": requestEvent.NostrId,
513 "appId": appId,
514 "responseEventId": responseEvent.ID,
515 "responseNostrEventId": resp.ID,
516 "relay": result.RelayURL,
517 }).WithError(result.Error).Error("failed to publish response event to relay")
518 }
519 }
520
521 if !publishSuccessful {
522 updateColumns["state"] = db.RESPONSE_EVENT_STATE_PUBLISH_FAILED
523 logger.Logger.WithFields(logrus.Fields{
524 "requestEventId": requestEvent.ID,
525 "requestNostrEventId": requestEvent.NostrId,
526 "appId": appId,
527 "responseEventId": responseEvent.ID,
528 "responseNostrEventId": resp.ID,
529 }).WithError(err).Error("Failed to publish reply")
530 } else {
531 updateColumns["state"] = db.RESPONSE_EVENT_STATE_PUBLISH_CONFIRMED
532 updateColumns["replied_at"] = time.Now()
533 logger.Logger.WithFields(logrus.Fields{
534 "requestEventId": requestEvent.ID,
535 "requestNostrEventId": requestEvent.NostrId,
536 "appId": appId,
537 "responseEventId": responseEvent.ID,
538 "responseNostrEventId": resp.ID,
539 }).Info("Published reply")
540 }
541
542 err = svc.db.
543 Model(&responseEvent).
544 Updates(updateColumns).
545 Error
546 if err != nil {
547 logger.Logger.WithFields(logrus.Fields{
548 "requestEventId": requestEvent.ID,
549 "requestNostrEventId": requestEvent.NostrId,
550 "appId": appId,
551 "responseEventId": responseEvent.ID,
552 "responseNostrEventId": resp.ID,
553 }).WithError(err).Error("Failed to update response/reply event")
554 return err
555 }
556
557 return nil
558 }
559