diff --git a/alby/alby_oauth_service.go b/alby/alby_oauth_service.go index 13bb554e..faf988a4 100644 --- a/alby/alby_oauth_service.go +++ b/alby/alby_oauth_service.go @@ -535,6 +535,7 @@ func (svc *albyOAuthService) LinkAccount(ctx context.Context, lnClient lnclient. scopes, false, nil, + svc.keys.GetBIP32ChildKey, ) if err != nil { diff --git a/api/api.go b/api/api.go index fcc6798e..56332608 100644 --- a/api/api.go +++ b/api/api.go @@ -2,7 +2,6 @@ package api import ( "context" - "encoding/hex" "encoding/json" "errors" "fmt" @@ -30,7 +29,6 @@ import ( "github.com/getAlby/hub/service/keys" "github.com/getAlby/hub/utils" "github.com/getAlby/hub/version" - "github.com/nbd-wtf/go-nostr" ) type api struct { @@ -81,7 +79,9 @@ func (api *api) CreateApp(ctx context.Context, createAppRequest *CreateAppReques expiresAt, createAppRequest.Scopes, createAppRequest.Isolated, - createAppRequest.Metadata) + createAppRequest.Metadata, + api.svc.GetKeys().GetBIP32ChildKey, + ) if err != nil { return nil, err @@ -100,21 +100,12 @@ func (api *api) CreateApp(ctx context.Context, createAppRequest *CreateAppReques return nil, err } - appWalletKey, err := api.keys.GetBIP32ChildKey(uint32(app.ID)) - if err != nil { - fmt.Println("error creating child key: ", err) - return nil, err - } - fmt.Println("!+!+!+!+!+!+!+ app secret key: ", hex.EncodeToString(appWalletKey.Serialize())) - appWalletPubKey, _ := nostr.GetPublicKey(hex.EncodeToString(appWalletKey.Serialize())) - fmt.Println("!+!+!+!+!+!+!+ app public key: ", appWalletPubKey) - if createAppRequest.ReturnTo != "" { returnToUrl, err := url.Parse(createAppRequest.ReturnTo) if err == nil { query := returnToUrl.Query() query.Add("relay", relayUrl) - query.Add("pubkey", appWalletPubKey) + query.Add("pubkey", app.WalletChildPubkey) if lightningAddress != "" && !app.Isolated { query.Add("lud16", lightningAddress) } @@ -127,14 +118,7 @@ func (api *api) CreateApp(ctx context.Context, createAppRequest *CreateAppReques if lightningAddress != "" && !app.Isolated { lud16 = fmt.Sprintf("&lud16=%s", lightningAddress) } - responseBody.PairingUri = fmt.Sprintf("nostr+walletconnect://%s?relay=%s&secret=%s%s", appWalletPubKey, relayUrl, pairingSecretKey, lud16) - - fmt.Println("~+~+~+~+~+~+~+~+~+ GOING TO SUBSCRIBE TO NEW APP WALLET!!!!!!!!!!!!!! ") - err = api.svc.SubscribeToAppRequests(ctx, appWalletPubKey) - if err != nil { - fmt.Println("error subscribing to new app wallet key: ", err) - return nil, err - } + responseBody.PairingUri = fmt.Sprintf("nostr+walletconnect://%s?relay=%s&secret=%s%s", app.WalletChildPubkey, relayUrl, pairingSecretKey, lud16) return responseBody, nil } @@ -234,7 +218,7 @@ func (api *api) UpdateApp(userApp *db.App, updateAppRequest *UpdateAppRequest) e } func (api *api) DeleteApp(userApp *db.App) error { - return api.db.Delete(userApp).Error + return api.dbSvc.DeleteApp(userApp) } func (api *api) GetApp(dbApp *db.App) *App { diff --git a/db/db_service.go b/db/db_service.go index 03a386ce..f2fb14c7 100644 --- a/db/db_service.go +++ b/db/db_service.go @@ -28,7 +28,17 @@ func NewDBService(db *gorm.DB, eventPublisher events.EventPublisher) *dbService } } -func (svc *dbService) CreateApp(name string, pubkey string, maxAmountSat uint64, budgetRenewal string, expiresAt *time.Time, scopes []string, isolated bool, metadata map[string]interface{}) (*App, string, error) { +func (svc *dbService) CreateApp( + name string, + pubkey string, + maxAmountSat uint64, + budgetRenewal string, + expiresAt *time.Time, + scopes []string, + isolated bool, + metadata map[string]interface{}, + walletChildPrivKeyGeneratorFunc func(appId uint32) (string, error), +) (*App, string, error) { if isolated && (slices.Contains(scopes, constants.SIGN_MESSAGE_SCOPE)) { // cannot sign messages because the isolated app is a custodial subaccount return nil, "", errors.New("isolated app cannot have sign_message scope") @@ -59,7 +69,7 @@ func (svc *dbService) CreateApp(name string, pubkey string, maxAmountSat uint64, } } - app := App{Name: name, NostrPubkey: pairingPublicKey, walletChildIdx: 2, Isolated: isolated, Metadata: datatypes.JSON(metadataBytes)} + app := App{Name: name, NostrPubkey: pairingPublicKey, Isolated: isolated, Metadata: datatypes.JSON(metadataBytes)} err := svc.db.Transaction(func(tx *gorm.DB) error { err := tx.Save(&app).Error @@ -82,6 +92,21 @@ func (svc *dbService) CreateApp(name string, pubkey string, maxAmountSat uint64, } } + appWalletChildPrivKey, err := walletChildPrivKeyGeneratorFunc(uint32(app.ID)) + if err != nil { + return fmt.Errorf("error generating wallet child private key: %w", err) + } + + app.WalletChildPubkey, err = nostr.GetPublicKey(appWalletChildPrivKey) + if err != nil { + return fmt.Errorf("error generating wallet child public key: %w", err) + } + + err = tx.Model(&App{}).Where("id", app.ID).Update("wallet_child_pubkey", app.WalletChildPubkey).Error + if err != nil { + return err + } + // commit transaction return nil }) @@ -94,9 +119,28 @@ func (svc *dbService) CreateApp(name string, pubkey string, maxAmountSat uint64, svc.eventPublisher.Publish(&events.Event{ Event: "app_created", Properties: map[string]interface{}{ - "name": name, + "name": name, + "id": app.ID, + "walletChildPubkey": app.WalletChildPubkey, }, }) return &app, pairingSecretKey, nil } + +func (svc *dbService) DeleteApp(app *App) error { + + err := svc.db.Delete(app).Error + if err != nil { + return err + } + svc.eventPublisher.Publish(&events.Event{ + Event: "app_deleted", + Properties: map[string]interface{}{ + "name": app.Name, + "id": app.ID, + "walletChildPubkey": app.WalletChildPubkey, + }, + }) + return nil +} diff --git a/db/migrations/202410141503_wallet_child_idx.go b/db/migrations/202410141503_wallet_child_pubkey.go similarity index 55% rename from db/migrations/202410141503_wallet_child_idx.go rename to db/migrations/202410141503_wallet_child_pubkey.go index 2f795d71..4325b155 100644 --- a/db/migrations/202410141503_wallet_child_idx.go +++ b/db/migrations/202410141503_wallet_child_pubkey.go @@ -7,13 +7,12 @@ import ( "gorm.io/gorm" ) -var _202410141503_wallet_child_idx = &gormigrate.Migration{ - ID: "202410141503_wallet_child_idx", +var _202410141503_wallet_child_pubkey = &gormigrate.Migration{ + ID: "202410141503_wallet_child_pubkey", Migrate: func(tx *gorm.DB) error { if err := tx.Exec(` - ALTER TABLE apps ADD COLUMN wallet_child_idx INTEGER; - CREATE UNIQUE INDEX idx_wallet_child_idx ON apps (wallet_child_idx); + ALTER TABLE apps ADD COLUMN wallet_child_pubkey TEXT; `).Error; err != nil { return err } diff --git a/db/migrations/migrate.go b/db/migrations/migrate.go index 27f9db8a..b9ef132c 100644 --- a/db/migrations/migrate.go +++ b/db/migrations/migrate.go @@ -22,7 +22,7 @@ func Migrate(gormDB *gorm.DB) error { _202408061737_add_boostagrams_and_use_json, _202408191242_transaction_failure_reason, _202408291715_app_metadata, - _202410141503_wallet_child_idx, + _202410141503_wallet_child_pubkey, }) return m.Migrate() diff --git a/db/models.go b/db/models.go index f93f267b..acba8edc 100644 --- a/db/models.go +++ b/db/models.go @@ -16,15 +16,15 @@ type UserConfig struct { } type App struct { - ID uint - Name string `validate:"required"` - Description string - NostrPubkey string `validate:"required"` - walletChildIdx uint `validate:"required"` - CreatedAt time.Time - UpdatedAt time.Time - Isolated bool - Metadata datatypes.JSON + ID uint + Name string `validate:"required"` + Description string + NostrPubkey string `validate:"required"` + WalletChildPubkey string + CreatedAt time.Time + UpdatedAt time.Time + Isolated bool + Metadata datatypes.JSON } type AppPermission struct { @@ -88,7 +88,8 @@ type Transaction struct { } type DBService interface { - CreateApp(name string, pubkey string, maxAmountSat uint64, budgetRenewal string, expiresAt *time.Time, scopes []string, isolated bool, metadata map[string]interface{}) (*App, string, error) + CreateApp(name string, pubkey string, maxAmountSat uint64, budgetRenewal string, expiresAt *time.Time, scopes []string, isolated bool, metadata map[string]interface{}, walletChildPrivKeyGeneratorFunc func(uint32) (string, error)) (*App, string, error) + DeleteApp(app *App) error } const ( diff --git a/nip47/event_handler.go b/nip47/event_handler.go index 2e5d8a5f..721fd254 100644 --- a/nip47/event_handler.go +++ b/nip47/event_handler.go @@ -2,7 +2,6 @@ package nip47 import ( "context" - "encoding/hex" "encoding/json" "errors" "fmt" @@ -46,6 +45,33 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela }).Error("invalid event signature") return } + pTag := event.Tags.GetFirst([]string{"p"}) + if pTag == nil { + logger.Logger.WithFields(logrus.Fields{ + "requestEventNostrId": event.ID, + "eventKind": event.Kind, + }).Error("invalid event, missing p tag") + return + } + appWalletPubKey := pTag.Value() + + app := db.App{} + err = svc.db.First(&app, &db.App{ + NostrPubkey: event.PubKey, + }).Error + + appWalletPrivKey := svc.keys.GetNostrSecretKey() + + if appWalletPubKey != svc.keys.GetNostrPublicKey() { + // This is a new child key derived from master using app ID as index + appWalletPrivKey, err = svc.keys.GetBIP32ChildKey(uint32(app.ID)) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "appId": app.ID, + }).WithError(err).Error("error deriving child key") + return + } + } ss, err := nip04.ComputeSharedSecret(event.PubKey, svc.keys.GetNostrSecretKey()) if err != nil { @@ -87,12 +113,6 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela return } - app := db.App{} - - err = svc.db.First(&app, &db.App{ - NostrPubkey: event.PubKey, - }).Error - if err != nil { logger.Logger.WithFields(logrus.Fields{ "nostrPubkey": event.PubKey, @@ -162,15 +182,6 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela "appId": app.ID, }).Debug("App found for nostr event") - appWalletPrivKeyBip32, err := svc.keys.GetBIP32ChildKey(uint32(app.ID)) - if err != nil { - logger.Logger.WithFields(logrus.Fields{ - "appId": app.ID, - }).WithError(err).Error("error deriving child key") - return - } - appWalletPrivKey, _ := nostr.GetPublicKey(hex.EncodeToString(appWalletPrivKeyBip32.Serialize())) - //to be extra safe, decrypt using the key found from the app ss, err = nip04.ComputeSharedSecret(app.NostrPubkey, appWalletPrivKey) if err != nil { @@ -368,7 +379,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela } } -func (svc *nip47Service) CreateResponse(initialEvent *nostr.Event, content interface{}, tags nostr.Tags, ss []byte, walletPrivKey string) (result *nostr.Event, err error) { +func (svc *nip47Service) CreateResponse(initialEvent *nostr.Event, content interface{}, tags nostr.Tags, ss []byte, appWalletPrivKey string) (result *nostr.Event, err error) { payloadBytes, err := json.Marshal(content) if err != nil { return nil, err @@ -381,7 +392,7 @@ func (svc *nip47Service) CreateResponse(initialEvent *nostr.Event, content inter allTags := nostr.Tags{[]string{"p", initialEvent.PubKey}, []string{"e", initialEvent.ID}} allTags = append(allTags, tags...) - appWalletPubKey, _ := nostr.GetPublicKey(walletPrivKey) + appWalletPubKey, _ := nostr.GetPublicKey(appWalletPrivKey) resp := &nostr.Event{ PubKey: appWalletPubKey, @@ -390,7 +401,7 @@ func (svc *nip47Service) CreateResponse(initialEvent *nostr.Event, content inter Tags: allTags, Content: msg, } - err = resp.Sign(walletPrivKey) + err = resp.Sign(appWalletPrivKey) if err != nil { return nil, err } diff --git a/nip47/nip47_service.go b/nip47/nip47_service.go index 2e9eeb3e..577823fb 100644 --- a/nip47/nip47_service.go +++ b/nip47/nip47_service.go @@ -2,7 +2,6 @@ package nip47 import ( "context" - "github.com/getAlby/hub/config" "github.com/getAlby/hub/events" "github.com/getAlby/hub/lnclient" @@ -29,7 +28,8 @@ type Nip47Service interface { events.EventSubscriber StartNotifier(ctx context.Context, relay *nostr.Relay, lnClient lnclient.LNClient) HandleEvent(ctx context.Context, relay nostrmodels.Relay, event *nostr.Event, lnClient lnclient.LNClient) - PublishNip47Info(ctx context.Context, relay nostrmodels.Relay, lnClient lnclient.LNClient) error + PublishNip47Info(ctx context.Context, relay nostrmodels.Relay, appWalletPubKey string, appWalletPrivKey string, lnClient lnclient.LNClient) (*nostr.Event, error) + PublishNip47InfoDeletion(ctx context.Context, relay nostrmodels.Relay, appWalletPubKey string, appWalletPrivKey string, infoEventId string) error CreateResponse(initialEvent *nostr.Event, content interface{}, tags nostr.Tags, ss []byte, walletPrivKey string) (result *nostr.Event, err error) } diff --git a/nip47/notifications/nip47_notifier.go b/nip47/notifications/nip47_notifier.go index f1974671..3c4ece94 100644 --- a/nip47/notifications/nip47_notifier.go +++ b/nip47/notifications/nip47_notifier.go @@ -2,7 +2,6 @@ package notifications import ( "context" - "encoding/hex" "encoding/json" "github.com/getAlby/hub/config" @@ -109,7 +108,7 @@ func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App "appId": app.ID, }).Debug("Notifying subscriber") - appWalletPrivKeyBIP32, err := notifier.keys.GetBIP32ChildKey(uint32(app.ID)) + appWalletPrivKey, err := notifier.keys.GetBIP32ChildKey(uint32(app.ID)) if err != nil { logger.Logger.WithFields(logrus.Fields{ "notification": notification, @@ -117,7 +116,6 @@ func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App }).WithError(err).Error("error derivingchild key") return } - appWalletPrivKey := hex.EncodeToString(appWalletPrivKeyBIP32.Serialize()) ss, err := nip04.ComputeSharedSecret(app.NostrPubkey, appWalletPrivKey) if err != nil { diff --git a/nip47/publish_nip47_info.go b/nip47/publish_nip47_info.go index 259be1b3..35cd1ab6 100644 --- a/nip47/publish_nip47_info.go +++ b/nip47/publish_nip47_info.go @@ -3,6 +3,7 @@ package nip47 import ( "context" "fmt" + "strconv" "strings" "github.com/getAlby/hub/lnclient" @@ -11,7 +12,7 @@ import ( "github.com/nbd-wtf/go-nostr" ) -func (svc *nip47Service) PublishNip47Info(ctx context.Context, relay nostrmodels.Relay, lnClient lnclient.LNClient) error { +func (svc *nip47Service) PublishNip47Info(ctx context.Context, relay nostrmodels.Relay, appWalletPubKey string, appWalletPrivKey string, lnClient lnclient.LNClient) (*nostr.Event, error) { capabilities := lnClient.GetSupportedNIP47Methods() if len(lnClient.GetSupportedNIP47NotificationTypes()) > 0 { capabilities = append(capabilities, "notifications") @@ -21,9 +22,27 @@ func (svc *nip47Service) PublishNip47Info(ctx context.Context, relay nostrmodels ev.Kind = models.INFO_EVENT_KIND ev.Content = strings.Join(capabilities, " ") ev.CreatedAt = nostr.Now() - ev.PubKey = svc.keys.GetNostrPublicKey() + ev.PubKey = appWalletPubKey ev.Tags = nostr.Tags{[]string{"notifications", strings.Join(lnClient.GetSupportedNIP47NotificationTypes(), " ")}} - err := ev.Sign(svc.keys.GetNostrSecretKey()) + err := ev.Sign(appWalletPrivKey) + if err != nil { + return nil, err + } + err = relay.Publish(ctx, *ev) + if err != nil { + return nil, fmt.Errorf("nostr publish not successful: %s", err) + } + return ev, nil +} + +func (svc *nip47Service) PublishNip47InfoDeletion(ctx context.Context, relay nostrmodels.Relay, appWalletPubKey string, appWalletPrivKey string, infoEventId string) error { + ev := &nostr.Event{} + ev.Kind = nostr.KindDeletion + ev.Content = "deleting nip47 info since app connection for this key was deleted" + ev.Tags = nostr.Tags{[]string{"e", infoEventId}, []string{"k", strconv.Itoa(models.INFO_EVENT_KIND)}} + ev.CreatedAt = nostr.Now() + ev.PubKey = appWalletPubKey + err := ev.Sign(appWalletPrivKey) if err != nil { return err } diff --git a/service/keys/keys.go b/service/keys/keys.go index e876f69a..f5a3cd65 100644 --- a/service/keys/keys.go +++ b/service/keys/keys.go @@ -17,7 +17,7 @@ type Keys interface { // Wallet Service Nostr secret key GetNostrSecretKey() string // GetBIP32ChildKey derives a BIP32 child key from the nostrSecretKey given a child key index - GetBIP32ChildKey(childIndex uint32) (*btcec.PrivateKey, error) + GetBIP32ChildKey(childIndex uint32) (string, error) } type keys struct { @@ -58,28 +58,28 @@ func (keys *keys) GetNostrSecretKey() string { return keys.nostrSecretKey } -func (keys *keys) GetBIP32ChildKey(childIndex uint32) (*btcec.PrivateKey, error) { +func (keys *keys) GetBIP32ChildKey(childIndex uint32) (string, error) { // Convert nostrSecretKey to btcec private key privKeyBytes, err := hex.DecodeString(keys.nostrSecretKey) if err != nil { - return nil, err + return "", err } privKey, _ := btcec.PrivKeyFromBytes(privKeyBytes) // Create a BIP32 master key from the private key masterKey, err := bip32.NewMasterKey(privKey.Serialize()) if err != nil { - return nil, err + return "", err } // Derive child key childKey, err := masterKey.NewChildKey(childIndex) if err != nil { - return nil, err + return "", err } // Convert child key to btcec private key childPrivKey, _ := btcec.PrivKeyFromBytes(childKey.Key) - return childPrivKey, nil + return hex.EncodeToString(childPrivKey.Serialize()), nil } diff --git a/service/models.go b/service/models.go index 27688f77..edb41bd7 100644 --- a/service/models.go +++ b/service/models.go @@ -1,8 +1,6 @@ package service import ( - "context" - "github.com/getAlby/hub/alby" "github.com/getAlby/hub/config" "github.com/getAlby/hub/events" @@ -16,7 +14,6 @@ type Service interface { StartApp(encryptionKey string) error StopApp() Shutdown() - SubscribeToAppRequests(ctx context.Context, appWalletPubKey string) error // TODO: remove getters (currently used by http / wails services) GetAlbyOAuthSvc() alby.AlbyOAuthService diff --git a/service/start.go b/service/start.go index 877918d1..58f8aad2 100644 --- a/service/start.go +++ b/service/start.go @@ -8,6 +8,8 @@ import ( "strconv" "time" + "github.com/getAlby/hub/db" + "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" "github.com/sirupsen/logrus" @@ -89,22 +91,16 @@ func (svc *service) startNostr(ctx context.Context, encryptionKey string) error }).WithError(err).Error("Failed to connect to relay") continue } - waitToReconnectSeconds = 0 - //publish event with NIP-47 info - err = svc.nip47Service.PublishNip47Info(ctx, relay, svc.lnClient) - if err != nil { - logger.Logger.WithError(err).Error("Could not publish NIP47 info") - } + // start each app wallet subscription which have a child derived wallet key + svc.startAllExistingAppsWalletSubscriptions(ctx, relay) - logger.Logger.Info("Subscribing to events") - sub, err := relay.Subscribe(ctx, svc.createFilters(svc.keys.GetNostrPublicKey())) - if err != nil { - logger.Logger.WithError(err).Error("Failed to subscribe to events") - continue - } - err = svc.StartSubscription(sub.Context, sub) + // register a subscriber for events of "app_created" which handles creation of nostr subscription for new app + svc.eventPublisher.RegisterSubscriber(&createAppSubscriber{svc: svc, relay: relay}) + + // legacy single wallet subscription + err = svc.startAppWalletSubscription(ctx, relay, svc.keys.GetNostrPublicKey(), svc.keys.GetNostrSecretKey()) if err != nil { //err being non-nil means that we have an error on the websocket error channel. In this case we just try to reconnect. logger.Logger.WithError(err).Error("Got an error from the relay while listening to subscription.") @@ -119,82 +115,132 @@ func (svc *service) startNostr(ctx context.Context, encryptionKey string) error return nil } -func (svc *service) SubscribeToAppRequests(ctx context.Context, appWalletPubKey string) error { - relayUrl := svc.cfg.GetRelayUrl() - go func() { - // ensure the relay is properly disconnected before exiting - defer svc.wg.Done() - //Start infinite loop which will be only broken by canceling ctx (SIGINT) - var relay *nostr.Relay - waitToReconnectSeconds := 0 - - for i := 0; ; i++ { - - // wait for a delay if any before retrying - if waitToReconnectSeconds > 0 { - contextCancelled := false +type createAppSubscriber struct { + events.EventSubscriber + svc *service + relay *nostr.Relay +} - select { - case <-ctx.Done(): //context cancelled - logger.Logger.Info("service context cancelled while waiting for retry") - contextCancelled = true - case <-time.After(time.Duration(waitToReconnectSeconds) * time.Second): //timeout +func (s *createAppSubscriber) ConsumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) { + switch event.Event { + case "app_created": + properties, ok := event.Properties.(map[string]interface{}) + if !ok { + logger.Logger.WithField("event", event).Error("Failed to cast event.Properties to map") + return + } + walletChildPubkey, ok := properties["walletChildPubkey"].(string) + if !ok { + logger.Logger.WithField("event", event).Error("Failed to get app walletChildPubkey") + return + } + id, ok := properties["id"].(uint) + if !ok { + logger.Logger.WithField("event", event).Error("Failed to get app id") + return + } + if walletChildPubkey != "" { + go func() { + appWalletPrivKey, err := s.svc.keys.GetBIP32ChildKey(uint32(id)) + if err != nil { + logger.Logger.WithError(err).Error("Failed to calculate app wallet priv key") } - if contextCancelled { - break + err = s.svc.startAppWalletSubscription(ctx, s.relay, walletChildPubkey, appWalletPrivKey) + if err != nil { + logger.Logger.WithError(err).Error("Failed to subscribe to wallet") } - } + // TODO what should we do? + }() + } + } +} - closeRelay(relay) +type deleteAppSubscriber struct { + events.EventSubscriber + walletChildPubkey string + relay *nostr.Relay + nostrSubscription *nostr.Subscription + svc *service + infoEventId string +} - //connect to the relay - logger.Logger.WithFields(logrus.Fields{ - "relay_url": relayUrl, - "iteration": i, - }).Info("Connecting to the relay") +func (s *deleteAppSubscriber) ConsumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) { + switch event.Event { + case "app_deleted": + properties, ok := event.Properties.(map[string]interface{}) + if !ok { + logger.Logger.WithField("event", event).Error("Failed to cast event.Properties to map") + return + } - relay, err := nostr.RelayConnect(ctx, relayUrl, nostr.WithNoticeHandler(svc.noticeHandler)) + walletChildPubkey, ok := properties["walletChildPubkey"].(string) + if s.walletChildPubkey == walletChildPubkey { + id, _ := properties["id"].(uint) + s.nostrSubscription.Unsub() + appWalletPrivKey, _ := s.svc.keys.GetBIP32ChildKey(uint32(id)) + err := s.svc.nip47Service.PublishNip47InfoDeletion(ctx, s.relay, walletChildPubkey, appWalletPrivKey, s.infoEventId) if err != nil { - // exponential backoff from 2 - 60 seconds - waitToReconnectSeconds = max(waitToReconnectSeconds, 1) - waitToReconnectSeconds *= 2 - waitToReconnectSeconds = min(waitToReconnectSeconds, 60) - logger.Logger.WithFields(logrus.Fields{ - "iteration": i, - "retry_seconds": waitToReconnectSeconds, - }).WithError(err).Error("Failed to connect to relay") - continue + logger.Logger.WithField("event", event).Error("Failed to publish nip47 info deletion") } + } + } +} - waitToReconnectSeconds = 0 +func (svc *service) startAllExistingAppsWalletSubscriptions(ctx context.Context, relay *nostr.Relay) { + var apps []db.App + result := svc.db.Where("wallet_child_pubkey != ?", "").Find(&apps) + if result.Error != nil { + logger.Logger.WithError(result.Error).Error("Failed to fetch App records with non-empty WalletChildPubkey") + return + } - //publish event with NIP-47 info - err = svc.nip47Service.PublishNip47Info(ctx, relay, svc.lnClient) - if err != nil { - logger.Logger.WithError(err).Error("Could not publish NIP47 info") + for _, app := range apps { + go func(app db.App) { + if app.WalletChildPubkey != "" { + err := svc.startAppWalletSubscription(ctx, relay, app.WalletChildPubkey, "") + if err != nil { + logger.Logger.WithError(err).Error("Failed to subscribe to wallet") + } } + }(app) + } +} - logger.Logger.Info("Subscribing to events") - sub, err := relay.Subscribe(ctx, svc.createFilters(appWalletPubKey)) - if err != nil { - logger.Logger.WithError(err).Error("Failed to subscribe to events") - continue - } - err = svc.StartSubscription(sub.Context, sub) - if err != nil { - //err being non-nil means that we have an error on the websocket error channel. In this case we just try to reconnect. - logger.Logger.WithError(err).Error("Got an error from the relay while listening to subscription.") - continue - } - //err being nil means that the context was canceled and we should exit the program. - break +func (svc *service) startAppWalletSubscription(ctx context.Context, relay *nostr.Relay, appWalletPubKey string, appWalletPrivKey string) error { + var infoEventId string + if appWalletPrivKey != "" { + infoEvent, err := svc.GetNip47Service().PublishNip47Info(ctx, relay, appWalletPubKey, appWalletPrivKey, svc.lnClient) + if err != nil { + logger.Logger.WithError(err).Error("Could not publish NIP47 info") } - closeRelay(relay) - logger.Logger.Info("Relay subroutine ended") - }() + infoEventId = infoEvent.ID + } + + sub, err := svc.subscribeToRequests(ctx, relay, appWalletPubKey) + if err != nil { + logger.Logger.WithError(err).Error("Failed to subscribe to app requests") + } + + // register a subscriber for "app_deleted" events, which handles nostr subscription cancel and nip47 info event deletion + svc.eventPublisher.RegisterSubscriber(&deleteAppSubscriber{nostrSubscription: sub, walletChildPubkey: appWalletPubKey, svc: svc, relay: relay, infoEventId: infoEventId}) + + err = svc.StartSubscription(sub.Context, sub) + if err != nil { + logger.Logger.WithError(err).Error("Got an error from the relay while listening to subscription.") + return err + } return nil } +func (svc *service) subscribeToRequests(ctx context.Context, relay *nostr.Relay, appWalletPubKey string) (*nostr.Subscription, error) { + logger.Logger.Info("Subscribing to events for wallet ", appWalletPubKey) + sub, err := relay.Subscribe(ctx, svc.createFilters(appWalletPubKey)) + if err != nil { + logger.Logger.WithError(err).Error("Failed to subscribe to events") + } + return sub, nil +} + func (svc *service) StartApp(encryptionKey string) error { if svc.lnClient != nil { return errors.New("app already started")