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