diff --git a/breez.go b/breez.go index 7a0c9790..2f798e08 100644 --- a/breez.go +++ b/breez.go @@ -31,6 +31,10 @@ func (BreezListener) OnEvent(e breez_sdk.BreezEvent) { } func NewBreezService(mnemonic, apiKey, inviteCode, workDir string) (result LNClient, err error) { + if mnemonic == "" || apiKey == "" || inviteCode == "" || workDir == "" { + return nil, errors.New("One or more required breez configuration are missing") + } + //create dir if not exists newpath := filepath.Join(".", workDir) err = os.MkdirAll(newpath, os.ModePerm) @@ -181,7 +185,26 @@ func (bs *BreezService) LookupInvoice(ctx context.Context, senderPubkey string, } func (bs *BreezService) ListTransactions(ctx context.Context, senderPubkey string, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []Nip47Transaction, err error) { - payments, err := bs.svc.ListPayments(breez_sdk.ListPaymentsRequest{}) + + request := breez_sdk.ListPaymentsRequest{} + if limit > 0 { + limit32 := uint32(limit) + request.Limit = &limit32 + } + if offset > 0 { + offset32 := uint32(offset) + request.Offset = &offset32 + } + if from > 0 { + from64 := int64(from) + request.FromTimestamp = &from64 + } + if until > 0 { + until64 := int64(until) + request.ToTimestamp = &until64 + } + + payments, err := bs.svc.ListPayments(request) if err != nil { return nil, err } @@ -225,15 +248,25 @@ func breezPaymentToTransaction(payment *breez_sdk.Payment) (*Nip47Transaction, e txType = "incoming" } - paymentRequest, err := decodepay.Decodepay(strings.ToLower(lnDetails.Data.Bolt11)) - if err != nil { - log.Printf("Failed to decode bolt11 invoice: %v", payment) - return nil, err - } + createdAt := payment.PaymentTime + var expiresAt *int64 + description := lnDetails.Data.Label + descriptionHash := "" + + if lnDetails.Data.Bolt11 != "" { + // TODO: Breez should provide these details so we don't need to manually decode the invoice + paymentRequest, err := decodepay.Decodepay(strings.ToLower(lnDetails.Data.Bolt11)) + if err != nil { + log.Printf("Failed to decode bolt11 invoice: %v", payment) + return nil, err + } - createdAt := int64(paymentRequest.CreatedAt) - expiresAtUnix := time.UnixMilli(int64(paymentRequest.CreatedAt) * 1000).Add(time.Duration(paymentRequest.Expiry) * time.Second).Unix() - expiresAt := &expiresAtUnix + createdAt = int64(paymentRequest.CreatedAt) + expiresAtUnix := time.UnixMilli(int64(paymentRequest.CreatedAt) * 1000).Add(time.Duration(paymentRequest.Expiry) * time.Second).Unix() + expiresAt = &expiresAtUnix + description = paymentRequest.Description + descriptionHash = paymentRequest.DescriptionHash + } tx := &Nip47Transaction{ Type: txType, @@ -245,8 +278,8 @@ func breezPaymentToTransaction(payment *breez_sdk.Payment) (*Nip47Transaction, e CreatedAt: createdAt, ExpiresAt: expiresAt, Metadata: nil, - Description: paymentRequest.Description, - DescriptionHash: paymentRequest.DescriptionHash, + Description: description, + DescriptionHash: descriptionHash, } if payment.Status == breez_sdk.PaymentStatusComplete { settledAt := payment.PaymentTime diff --git a/config.go b/config.go index 3f1eb60f..4c60c12b 100644 --- a/config.go +++ b/config.go @@ -6,6 +6,7 @@ import ( "os" "github.com/getAlby/nostr-wallet-connect/models/db" + "github.com/sirupsen/logrus" "gorm.io/gorm" "gorm.io/gorm/clause" ) @@ -19,6 +20,7 @@ const ( type AppConfig struct { Relay string `envconfig:"RELAY" default:"wss://relay.getalby.com/v1"` LNBackendType string `envconfig:"LN_BACKEND_TYPE"` + LNDAddress string `envconfig:"LND_ADDRESS"` LNDCertFile string `envconfig:"LND_CERT_FILE"` LNDMacaroonFile string `envconfig:"LND_MACAROON_FILE"` Workdir string `envconfig:"WORK_DIR" default:".data"` @@ -33,11 +35,13 @@ type Config struct { NostrSecretKey string NostrPublicKey string db *gorm.DB + logger *logrus.Logger } -func (cfg *Config) Init(db *gorm.DB, env *AppConfig) { +func (cfg *Config) Init(db *gorm.DB, env *AppConfig, logger *logrus.Logger) { cfg.db = db cfg.Env = env + cfg.logger = logger if cfg.Env.Relay != "" { cfg.SetUpdate("Relay", cfg.Env.Relay, "") @@ -45,19 +49,24 @@ func (cfg *Config) Init(db *gorm.DB, env *AppConfig) { if cfg.Env.LNBackendType != "" { cfg.SetUpdate("LNBackendType", cfg.Env.LNBackendType, "") } + if cfg.Env.LNDAddress != "" { + cfg.SetUpdate("LNDAddress", cfg.Env.LNDAddress, "") + } if cfg.Env.LNDCertFile != "" { certBytes, err := os.ReadFile(cfg.Env.LNDCertFile) if err != nil { - certHex := hex.EncodeToString(certBytes) - cfg.SetUpdate("LNDCertHex", certHex, "") + logger.Fatalf("Failed to read LND cert file: %v", err) } + certHex := hex.EncodeToString(certBytes) + cfg.SetUpdate("LNDCertHex", certHex, "") } if cfg.Env.LNDMacaroonFile != "" { macBytes, err := os.ReadFile(cfg.Env.LNDMacaroonFile) if err != nil { - macHex := hex.EncodeToString(macBytes) - cfg.SetUpdate("LNDMacaroonHex", macHex, "") + logger.Fatalf("Failed to read LND macaroon file: %v", err) } + macHex := hex.EncodeToString(macBytes) + cfg.SetUpdate("LNDMacaroonHex", macHex, "") } // set the cookie secret to the one from the env // if no cookie secret is configured we create a random one and store it in the DB @@ -86,33 +95,36 @@ func (cfg *Config) Get(key string, encryptionKey string) (string, error) { return value, nil } -func (cfg *Config) set(key string, value string, clauses clause.OnConflict, encryptionKey string) bool { +func (cfg *Config) set(key string, value string, clauses clause.OnConflict, encryptionKey string) { if encryptionKey != "" { encrypted, err := AesGcmEncrypt(value, encryptionKey) - if err == nil { - value = encrypted + if err != nil { + cfg.logger.Fatalf("Failed to encrypt: %v", err) } + value = encrypted } userConfig := db.UserConfig{Key: key, Value: value, Encrypted: encryptionKey != ""} result := cfg.db.Clauses(clauses).Create(&userConfig) - return result.Error == nil + if result.Error != nil { + cfg.logger.Fatalf("Failed to save key to config: %v", result.Error) + } } -func (cfg *Config) SetIgnore(key string, value string, encryptionKey string) bool { +func (cfg *Config) SetIgnore(key string, value string, encryptionKey string) { clauses := clause.OnConflict{ Columns: []clause.Column{{Name: "key"}}, DoNothing: true, } - return cfg.set(key, value, clauses, encryptionKey) + cfg.set(key, value, clauses, encryptionKey) } -func (cfg *Config) SetUpdate(key string, value string, encryptionKey string) bool { +func (cfg *Config) SetUpdate(key string, value string, encryptionKey string) { clauses := clause.OnConflict{ Columns: []clause.Column{{Name: "key"}}, DoUpdates: clause.AssignmentColumns([]string{"value"}), } - return cfg.set(key, value, clauses, encryptionKey) + cfg.set(key, value, clauses, encryptionKey) } func randomHex(n int) (string, error) { diff --git a/frontend/src/components/redirects/StartRedirect.tsx b/frontend/src/components/redirects/StartRedirect.tsx index 5283d1f0..15b05c10 100644 --- a/frontend/src/components/redirects/StartRedirect.tsx +++ b/frontend/src/components/redirects/StartRedirect.tsx @@ -8,7 +8,6 @@ export function StartRedirect({ children }: React.PropsWithChildren) { const location = useLocation(); const navigate = useNavigate(); - // TODO: re-add login redirect: https://github.com/getAlby/nostr-wallet-connect/commit/59b041886098dda4ff38191e3dd704ec36360673 React.useEffect(() => { if (!info || (info.setupCompleted && !info.running)) { return; diff --git a/lnd.go b/lnd.go index 5fbf1c0c..1d6cf349 100644 --- a/lnd.go +++ b/lnd.go @@ -311,6 +311,10 @@ func makePreimageHex() ([]byte, error) { } func NewLNDService(svc *Service, lndAddress, lndCertHex, lndMacaroonHex string) (result LNClient, err error) { + if lndAddress == "" || lndCertHex == "" || lndMacaroonHex == "" { + return nil, errors.New("One or more required LND configuration are missing") + } + lndClient, err := lnd.NewLNDclient(lnd.LNDoptions{ Address: lndAddress, CertHex: lndCertHex, diff --git a/service.go b/service.go index f6b1a27c..76f4d613 100644 --- a/service.go +++ b/service.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "strings" "sync" "time" @@ -29,13 +28,12 @@ import ( type Service struct { // config from .env only. Fetch dynamic config from db - cfg *Config - db *gorm.DB - lnClient LNClient - ReceivedEOS bool - Logger *logrus.Logger - ctx context.Context - wg *sync.WaitGroup + cfg *Config + db *gorm.DB + lnClient LNClient + Logger *logrus.Logger + ctx context.Context + wg *sync.WaitGroup } // TODO: move to service.go @@ -95,7 +93,7 @@ func NewService(ctx context.Context) (*Service, error) { ctx, _ = signal.NotifyContext(ctx, os.Interrupt) cfg := &Config{} - cfg.Init(db, appConfig) + cfg.Init(db, appConfig, logger) var wg sync.WaitGroup svc := &Service{ @@ -131,7 +129,6 @@ func (svc *Service) launchLNBackend(encryptionKey string) error { LNDAddress, _ := svc.cfg.Get("LNDAddress", encryptionKey) LNDCertHex, _ := svc.cfg.Get("LNDCertHex", encryptionKey) LNDMacaroonHex, _ := svc.cfg.Get("LNDMacaroonHex", encryptionKey) - lnClient, err = NewLNDService(svc, LNDAddress, LNDCertHex, LNDMacaroonHex) case lndBackend: BreezMnemonic, _ := svc.cfg.Get("BreezMnemonic", encryptionKey) @@ -165,12 +162,11 @@ func (svc *Service) noticeHandler(notice string) { func (svc *Service) StartSubscription(ctx context.Context, sub *nostr.Subscription) error { go func() { + // block till EOS is received <-sub.EndOfStoredEvents - svc.ReceivedEOS = true svc.Logger.Info("Received EOS") - }() - go func() { + // loop through incoming events for event := range sub.Events { go func(event *nostr.Event) { resp, err := svc.HandleEvent(ctx, event) @@ -255,10 +251,6 @@ func (svc *Service) StartSubscription(ctx context.Context, sub *nostr.Subscripti } func (svc *Service) HandleEvent(ctx context.Context, event *nostr.Event) (result *nostr.Event, err error) { - //don't process historical events - if !svc.ReceivedEOS { - return nil, nil - } svc.Logger.WithFields(logrus.Fields{ "eventId": event.ID, "eventKind": event.Kind, @@ -371,13 +363,9 @@ func (svc *Service) createResponse(initialEvent *nostr.Event, content interface{ func (svc *Service) GetMethods(app *App) []string { appPermissions := []AppPermission{} - findPermissionsResult := svc.db.Find(&appPermissions, &AppPermission{ + svc.db.Find(&appPermissions, &AppPermission{ AppId: app.ID, }) - if findPermissionsResult.RowsAffected == 0 { - // No permissions created for this app. It can do anything - return strings.Split(NIP_47_CAPABILITIES, ",") - } requestMethods := make([]string, 0, len(appPermissions)) for _, appPermission := range appPermissions { requestMethods = append(requestMethods, appPermission.RequestMethod) @@ -386,24 +374,9 @@ func (svc *Service) GetMethods(app *App) []string { } func (svc *Service) hasPermission(app *App, event *nostr.Event, requestMethod string, amount int64) (result bool, code string, message string) { - // find all permissions for the app - appPermissions := []AppPermission{} - findPermissionsResult := svc.db.Find(&appPermissions, &AppPermission{ - AppId: app.ID, - }) - if findPermissionsResult.RowsAffected == 0 { - // No permissions created for this app. It can do anything - svc.Logger.WithFields(logrus.Fields{ - "eventId": event.ID, - "requestMethod": requestMethod, - "appId": app.ID, - "pubkey": app.NostrPubkey, - }).Info("No permissions found for app") - return true, "", "" - } - appPermission := AppPermission{} - findPermissionResult := findPermissionsResult.Limit(1).Find(&appPermission, &AppPermission{ + findPermissionResult := svc.db.Find(&appPermission, &AppPermission{ + AppId: app.ID, RequestMethod: requestMethod, }) if findPermissionResult.RowsAffected == 0 { diff --git a/service_test.go b/service_test.go index da2a4b69..f74a82cc 100644 --- a/service_test.go +++ b/service_test.go @@ -149,14 +149,6 @@ func TestHandleEvent(t *testing.T) { ctx := context.TODO() svc, _ := createTestService(t) defer os.Remove(testDB) - //test not yet receivedEOS - res, err := svc.HandleEvent(ctx, &nostr.Event{ - Kind: NIP_47_REQUEST_KIND, - }) - assert.Nil(t, res) - assert.Nil(t, err) - //now signal that we are ready to receive events - svc.ReceivedEOS = true senderPrivkey := nostr.GeneratePrivateKey() senderPubkey, err := nostr.GetPublicKey(senderPrivkey) @@ -166,7 +158,7 @@ func TestHandleEvent(t *testing.T) { assert.NoError(t, err) payload, err := nip04.Encrypt(nip47PayJson, ss) assert.NoError(t, err) - res, err = svc.HandleEvent(ctx, &nostr.Event{ + res, err := svc.HandleEvent(ctx, &nostr.Event{ ID: "test_event_1", Kind: NIP_47_REQUEST_KIND, PubKey: senderPubkey, @@ -185,15 +177,6 @@ func TestHandleEvent(t *testing.T) { app := App{Name: "test", NostrPubkey: senderPubkey} err = svc.db.Save(&app).Error assert.NoError(t, err) - //test old payload - res, err = svc.HandleEvent(ctx, &nostr.Event{ - ID: "test_event_2", - Kind: NIP_47_REQUEST_KIND, - PubKey: senderPubkey, - Content: payload, - }) - assert.NoError(t, err) - assert.NotNil(t, res) //test new payload newPayload, err := nip04.Encrypt(nip47PayJson, ss) assert.NoError(t, err) @@ -212,7 +195,9 @@ func TestHandleEvent(t *testing.T) { } err = json.Unmarshal([]byte(decrypted), received) assert.NoError(t, err) - assert.Equal(t, received.Result.(*Nip47PayResponse).Preimage, "123preimage") + // this app has no permission + assert.Equal(t, received.Error.Code, NIP_47_ERROR_RESTRICTED) + malformedPayload, err := nip04.Encrypt(nip47PayJsonNoInvoice, ss) assert.NoError(t, err) res, err = svc.HandleEvent(ctx, &nostr.Event{ @@ -646,10 +631,9 @@ func createTestService(t *testing.T) (svc *Service, ln LNClient) { NostrSecretKey: sk, NostrPublicKey: pk, }, - db: gormDb, - lnClient: ln, - ReceivedEOS: false, - Logger: logger, + db: gormDb, + lnClient: ln, + Logger: logger, }, ln }