From 073d669e03e5cfeee7ab4f16b4e60fdc492a6df6 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Thu, 30 May 2024 00:06:06 +0700 Subject: [PATCH] Refactor: API cleanup (#338) --- alby/alby_http_service.go | 29 +- alby/{alby.go => alby_oauth_service.go} | 90 +- alby/models.go | 60 + api.go | 1552 ----------------- api/api.go | 643 +++++++ models/api/api.go => api/models.go | 62 +- backup/backup_service.go | 295 ++++ backup/models.go | 10 + aesgcm.go => config/aesgcm.go | 2 +- config.go => config/config.go | 70 +- models/config/config.go => config/models.go | 9 +- db/db_service.go | 79 + db/models.go | 83 + events/events.go | 22 - events/models.go | 25 + frontend/src/components/SidebarHint.tsx | 21 +- frontend/src/lib/utils.ts | 5 + frontend/src/screens/channels/Channels.tsx | 41 +- .../screens/channels/CurrentChannelOrder.tsx | 3 +- frontend/src/screens/setup/SetupPassword.tsx | 7 +- frontend/src/types.ts | 3 +- handle_balance_request.go | 17 +- handle_info_request.go | 15 +- handle_list_transactions_request.go | 17 +- handle_lookup_invoice_request.go | 25 +- handle_make_invoice_request.go | 19 +- handle_multi_pay_invoice_request.go | 33 +- handle_multi_pay_keysend_request.go | 27 +- handle_pay_keysend_request.go | 27 +- handle_payment_request.go | 33 +- handle_sign_message_request.go | 17 +- http_service.go => http/http_service.go | 197 ++- models/http/http.go => http/models.go | 1 - breez.go => lnclient/breez/breez.go | 25 +- breez_stub.go => lnclient/breez/breez_stub.go | 4 +- .../greenlight/greenlight.go | 96 +- .../greenlight/models.go | 3 +- ldk.go => lnclient/ldk/ldk.go | 348 ++-- .../ldk/ldk_event_broadcaster.go | 2 +- lnd.go => lnclient/lnd/lnd.go | 41 +- {lnd => lnclient/lnd/wrapper}/interface.go | 2 +- {lnd => lnclient/lnd/wrapper}/lnd.go | 2 +- .../lnclient.go => lnclient/models.go | 5 +- phoenixd.go => lnclient/phoenixd/phoenixd.go | 33 +- lsp/lsp_service.go | 621 +++++++ models/lsp/lsp.go => lsp/models.go | 19 +- main_http.go | 19 +- main_wails.go | 8 +- .../202403171120_delete_ldk_payments.go | 2 +- migrations/migrate.go | 2 +- models.go | 204 --- models/db/db.go | 13 - nip47/models.go | 127 ++ nip47_notifier.go | 34 +- nip47_notifier_test.go | 21 +- service.go | 298 ++-- service/models.go | 20 + service_test.go | 247 +-- start.go | 64 +- utils.go => utils/utils.go | 2 +- wails_app.go | 7 +- wails_handlers.go | 106 +- 62 files changed, 3106 insertions(+), 2808 deletions(-) rename alby/{alby.go => alby_oauth_service.go} (85%) create mode 100644 alby/models.go delete mode 100644 api.go create mode 100644 api/api.go rename models/api/api.go => api/models.go (65%) create mode 100644 backup/backup_service.go create mode 100644 backup/models.go rename aesgcm.go => config/aesgcm.go (99%) rename config.go => config/config.go (73%) rename models/config/config.go => config/models.go (89%) create mode 100644 db/db_service.go create mode 100644 db/models.go create mode 100644 events/models.go rename http_service.go => http/http_service.go (76%) rename models/http/http.go => http/models.go (70%) rename breez.go => lnclient/breez/breez.go (95%) rename breez_stub.go => lnclient/breez/breez_stub.go (77%) rename greenlight.go => lnclient/greenlight/greenlight.go (85%) rename models/greenlight/greenlight.go => lnclient/greenlight/models.go (97%) rename ldk.go => lnclient/ldk/ldk.go (74%) rename ldk_event_broadcaster.go => lnclient/ldk/ldk_event_broadcaster.go (99%) rename lnd.go => lnclient/lnd/lnd.go (92%) rename {lnd => lnclient/lnd/wrapper}/interface.go (99%) rename {lnd => lnclient/lnd/wrapper}/lnd.go (99%) rename models/lnclient/lnclient.go => lnclient/models.go (97%) rename phoenixd.go => lnclient/phoenixd/phoenixd.go (94%) create mode 100644 lsp/lsp_service.go rename models/lsp/lsp.go => lsp/models.go (80%) delete mode 100644 models.go delete mode 100644 models/db/db.go create mode 100644 service/models.go rename utils.go => utils/utils.go (99%) diff --git a/alby/alby_http_service.go b/alby/alby_http_service.go index 0136e54d..08597784 100644 --- a/alby/alby_http_service.go +++ b/alby/alby_http_service.go @@ -4,21 +4,22 @@ import ( "fmt" "net/http" - "github.com/getAlby/nostr-wallet-connect/models/api" - models "github.com/getAlby/nostr-wallet-connect/models/http" + "github.com/getAlby/nostr-wallet-connect/config" "github.com/labstack/echo/v4" "github.com/sirupsen/logrus" ) type AlbyHttpService struct { - albyOAuthSvc *AlbyOAuthService + albyOAuthSvc AlbyOAuthService logger *logrus.Logger + appConfig *config.AppConfig } -func NewAlbyHttpService(albyOAuthSvc *AlbyOAuthService, logger *logrus.Logger) *AlbyHttpService { +func NewAlbyHttpService(albyOAuthSvc AlbyOAuthService, logger *logrus.Logger, appConfig *config.AppConfig) *AlbyHttpService { return &AlbyHttpService{ albyOAuthSvc: albyOAuthSvc, logger: logger, + appConfig: appConfig, } } @@ -36,20 +37,20 @@ func (albyHttpSvc *AlbyHttpService) albyCallbackHandler(c echo.Context) error { err := albyHttpSvc.albyOAuthSvc.CallbackHandler(c.Request().Context(), code) if err != nil { albyHttpSvc.logger.WithError(err).Error("Failed to handle Alby OAuth callback") - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to handle Alby OAuth callback: %s", err.Error()), }) } - if albyHttpSvc.albyOAuthSvc.appConfig.IsDefaultClientId() { + if albyHttpSvc.appConfig.IsDefaultClientId() { // do not redirect if using default OAuth client // redirect will be handled by the frontend instead return c.NoContent(http.StatusNoContent) } - redirectUrl := albyHttpSvc.albyOAuthSvc.appConfig.FrontendUrl + redirectUrl := albyHttpSvc.appConfig.FrontendUrl if redirectUrl == "" { - redirectUrl = albyHttpSvc.albyOAuthSvc.appConfig.BaseUrl + redirectUrl = albyHttpSvc.appConfig.BaseUrl } return c.Redirect(http.StatusFound, redirectUrl) @@ -59,7 +60,7 @@ func (albyHttpSvc *AlbyHttpService) albyMeHandler(c echo.Context) error { me, err := albyHttpSvc.albyOAuthSvc.GetMe(c.Request().Context()) if err != nil { albyHttpSvc.logger.WithError(err).Error("Failed to request alby me endpoint") - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to request alby me endpoint: %s", err.Error()), }) } @@ -71,20 +72,20 @@ func (albyHttpSvc *AlbyHttpService) albyBalanceHandler(c echo.Context) error { balance, err := albyHttpSvc.albyOAuthSvc.GetBalance(c.Request().Context()) if err != nil { albyHttpSvc.logger.WithError(err).Error("Failed to request alby balance endpoint") - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to request alby balance endpoint: %s", err.Error()), }) } - return c.JSON(http.StatusOK, &api.AlbyBalanceResponse{ + return c.JSON(http.StatusOK, &AlbyBalanceResponse{ Sats: balance.Balance, }) } func (albyHttpSvc *AlbyHttpService) albyPayHandler(c echo.Context) error { - var payRequest api.AlbyPayRequest + var payRequest AlbyPayRequest if err := c.Bind(&payRequest); err != nil { - return c.JSON(http.StatusBadRequest, models.ErrorResponse{ + return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: fmt.Sprintf("Bad request: %s", err.Error()), }) } @@ -92,7 +93,7 @@ func (albyHttpSvc *AlbyHttpService) albyPayHandler(c echo.Context) error { err := albyHttpSvc.albyOAuthSvc.SendPayment(c.Request().Context(), payRequest.Invoice) if err != nil { albyHttpSvc.logger.WithError(err).Error("Failed to request alby pay endpoint") - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to request alby pay endpoint: %s", err.Error()), }) } diff --git a/alby/alby.go b/alby/alby_oauth_service.go similarity index 85% rename from alby/alby.go rename to alby/alby_oauth_service.go index 63b9988f..eb1ee312 100644 --- a/alby/alby.go +++ b/alby/alby_oauth_service.go @@ -8,52 +8,24 @@ import ( "fmt" "net/http" "strconv" + "strings" "sync" "time" + "github.com/getAlby/nostr-wallet-connect/config" + "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/events" - "github.com/getAlby/nostr-wallet-connect/models/api" - "github.com/getAlby/nostr-wallet-connect/models/config" "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/sirupsen/logrus" "golang.org/x/oauth2" ) -type AlbyOAuthService struct { +type albyOAuthService struct { appConfig *config.AppConfig config config.Config oauthConf *oauth2.Config logger *logrus.Logger - api api.API -} - -// TODO: move to models/alby -type AlbyMe struct { - Identifier string `json:"identifier"` - NPub string `json:"nostr_pubkey"` - LightningAddress string `json:"lightning_address"` - Email string `json:"email"` - Name string `json:"name"` - Avatar string `json:"avatar"` - KeysendPubkey string `json:"keysend_pubkey"` - SharedNode bool `json:"shared_node"` -} - -type AlbyBalance struct { - Balance int64 `json:"balance"` - Unit string `json:"unit"` - Currency string `json:"currency"` -} - -type ChannelPeerSuggestion struct { - Network string `json:"network"` - PaymentMethod string `json:"paymentMethod"` - Pubkey string `json:"pubkey"` - Host string `json:"host"` - MinimumChannelSize uint64 `json:"minimumChannelSize"` - Name string `json:"name"` - Image string `json:"image"` - Lsp string `json:"lsp"` + dbSvc db.DBService } const ( @@ -63,7 +35,7 @@ const ( userIdentifierKey = "AlbyUserIdentifier" ) -func NewAlbyOAuthService(logger *logrus.Logger, kvStore config.Config, appConfig *config.AppConfig, api api.API) *AlbyOAuthService { +func NewAlbyOAuthService(logger *logrus.Logger, config config.Config, appConfig *config.AppConfig, dbSvc db.DBService) *albyOAuthService { conf := &oauth2.Config{ ClientID: appConfig.AlbyClientId, ClientSecret: appConfig.AlbyClientSecret, @@ -81,17 +53,17 @@ func NewAlbyOAuthService(logger *logrus.Logger, kvStore config.Config, appConfig conf.RedirectURL = appConfig.BaseUrl + "/api/alby/callback" } - albyOAuthSvc := &AlbyOAuthService{ + albyOAuthSvc := &albyOAuthService{ appConfig: appConfig, oauthConf: conf, - config: kvStore, + config: config, logger: logger, - api: api, + dbSvc: dbSvc, } return albyOAuthSvc } -func (svc *AlbyOAuthService) CallbackHandler(ctx context.Context, code string) error { +func (svc *albyOAuthService) CallbackHandler(ctx context.Context, code string) error { token, err := svc.oauthConf.Exchange(ctx, code) if err != nil { svc.logger.WithError(err).Error("Failed to exchange token") @@ -122,7 +94,7 @@ func (svc *AlbyOAuthService) CallbackHandler(ctx context.Context, code string) e return nil } -func (svc *AlbyOAuthService) GetUserIdentifier() (string, error) { +func (svc *albyOAuthService) GetUserIdentifier() (string, error) { userIdentifier, err := svc.config.Get(userIdentifierKey, "") if err != nil { svc.logger.WithError(err).Error("Failed to fetch user identifier from user configs") @@ -131,7 +103,7 @@ func (svc *AlbyOAuthService) GetUserIdentifier() (string, error) { return userIdentifier, nil } -func (svc *AlbyOAuthService) IsConnected(ctx context.Context) bool { +func (svc *albyOAuthService) IsConnected(ctx context.Context) bool { token, err := svc.fetchUserToken(ctx) if err != nil { svc.logger.WithError(err).Error("Failed to check fetch token") @@ -139,7 +111,7 @@ func (svc *AlbyOAuthService) IsConnected(ctx context.Context) bool { return token != nil } -func (svc *AlbyOAuthService) saveToken(token *oauth2.Token) { +func (svc *albyOAuthService) saveToken(token *oauth2.Token) { svc.config.SetUpdate(accessTokenExpiryKey, strconv.FormatInt(token.Expiry.Unix(), 10), "") svc.config.SetUpdate(accessTokenKey, token.AccessToken, "") svc.config.SetUpdate(refreshTokenKey, token.RefreshToken, "") @@ -147,7 +119,7 @@ func (svc *AlbyOAuthService) saveToken(token *oauth2.Token) { var tokenMutex sync.Mutex -func (svc *AlbyOAuthService) fetchUserToken(ctx context.Context) (*oauth2.Token, error) { +func (svc *albyOAuthService) fetchUserToken(ctx context.Context) (*oauth2.Token, error) { tokenMutex.Lock() defer tokenMutex.Unlock() accessToken, err := svc.config.Get(accessTokenKey, "") @@ -202,8 +174,7 @@ func (svc *AlbyOAuthService) fetchUserToken(ctx context.Context) (*oauth2.Token, return newToken, nil } -func (svc *AlbyOAuthService) GetMe(ctx context.Context) (*AlbyMe, error) { - +func (svc *albyOAuthService) GetMe(ctx context.Context) (*AlbyMe, error) { token, err := svc.fetchUserToken(ctx) if err != nil { svc.logger.WithError(err).Error("Failed to fetch user token") @@ -237,7 +208,7 @@ func (svc *AlbyOAuthService) GetMe(ctx context.Context) (*AlbyMe, error) { return me, nil } -func (svc *AlbyOAuthService) GetBalance(ctx context.Context) (*AlbyBalance, error) { +func (svc *albyOAuthService) GetBalance(ctx context.Context) (*AlbyBalance, error) { token, err := svc.fetchUserToken(ctx) if err != nil { @@ -271,7 +242,7 @@ func (svc *AlbyOAuthService) GetBalance(ctx context.Context) (*AlbyBalance, erro return balance, nil } -func (svc *AlbyOAuthService) SendPayment(ctx context.Context, invoice string) error { +func (svc *albyOAuthService) SendPayment(ctx context.Context, invoice string) error { token, err := svc.fetchUserToken(ctx) if err != nil { svc.logger.WithError(err).Error("Failed to fetch user token") @@ -356,27 +327,28 @@ func (svc *AlbyOAuthService) SendPayment(ctx context.Context, invoice string) er return nil } -func (svc *AlbyOAuthService) GetAuthUrl() string { +func (svc *albyOAuthService) GetAuthUrl() string { if svc.appConfig.AlbyClientId == "" || svc.appConfig.AlbyClientSecret == "" { svc.logger.Fatalf("No ALBY_OAUTH_CLIENT_ID or ALBY_OAUTH_CLIENT_SECRET set") } return svc.oauthConf.AuthCodeURL("unused") } -func (svc *AlbyOAuthService) LinkAccount(ctx context.Context) error { +func (svc *albyOAuthService) LinkAccount(ctx context.Context) error { connectionPubkey, err := svc.createAlbyAccountNWCNode(ctx) if err != nil { svc.logger.WithError(err).Error("Failed to create alby account nwc node") return err } - app, err := svc.api.CreateApp(&api.CreateAppRequest{ - Name: "getalby.com", - Pubkey: connectionPubkey, - MaxAmount: 1_000_000, - BudgetRenewal: nip47.BUDGET_RENEWAL_MONTHLY, - RequestMethods: nip47.CAPABILITIES, - }) + app, _, err := svc.dbSvc.CreateApp( + "getalby.com", + connectionPubkey, + 1_000_000, + nip47.BUDGET_RENEWAL_MONTHLY, + nil, + strings.Split(nip47.CAPABILITIES, " "), + ) if err != nil { svc.logger.WithError(err).Error("Failed to create app connection") @@ -396,7 +368,7 @@ func (svc *AlbyOAuthService) LinkAccount(ctx context.Context) error { return nil } -func (svc *AlbyOAuthService) ConsumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) error { +func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) error { // TODO: rename this config option to be specific to the alby API if !svc.appConfig.LogEvents { svc.logger.WithField("event", event).Debug("Skipped sending to alby events API") @@ -481,7 +453,7 @@ func (svc *AlbyOAuthService) ConsumeEvent(ctx context.Context, event *events.Eve return nil } -func (svc *AlbyOAuthService) createAlbyAccountNWCNode(ctx context.Context) (string, error) { +func (svc *albyOAuthService) createAlbyAccountNWCNode(ctx context.Context) (string, error) { token, err := svc.fetchUserToken(ctx) if err != nil { svc.logger.WithError(err).Error("Failed to fetch user token") @@ -548,7 +520,7 @@ func (svc *AlbyOAuthService) createAlbyAccountNWCNode(ctx context.Context) (stri return responsePayload.Pubkey, nil } -func (svc *AlbyOAuthService) activateAlbyAccountNWCNode(ctx context.Context) error { +func (svc *albyOAuthService) activateAlbyAccountNWCNode(ctx context.Context) error { token, err := svc.fetchUserToken(ctx) if err != nil { svc.logger.WithError(err).Error("Failed to fetch user token") @@ -583,7 +555,7 @@ func (svc *AlbyOAuthService) activateAlbyAccountNWCNode(ctx context.Context) err return nil } -func (svc *AlbyOAuthService) GetChannelPeerSuggestions(ctx context.Context) ([]ChannelPeerSuggestion, error) { +func (svc *albyOAuthService) GetChannelPeerSuggestions(ctx context.Context) ([]ChannelPeerSuggestion, error) { token, err := svc.fetchUserToken(ctx) if err != nil { diff --git a/alby/models.go b/alby/models.go new file mode 100644 index 00000000..58061f8e --- /dev/null +++ b/alby/models.go @@ -0,0 +1,60 @@ +package alby + +import ( + "context" + + "github.com/getAlby/nostr-wallet-connect/events" +) + +type AlbyOAuthService interface { + events.EventSubscriber + GetChannelPeerSuggestions(ctx context.Context) ([]ChannelPeerSuggestion, error) + GetAuthUrl() string + GetUserIdentifier() (string, error) + IsConnected(ctx context.Context) bool + LinkAccount(ctx context.Context) error + CallbackHandler(ctx context.Context, code string) error + GetBalance(ctx context.Context) (*AlbyBalance, error) + GetMe(ctx context.Context) (*AlbyMe, error) + SendPayment(ctx context.Context, invoice string) error +} + +type AlbyBalanceResponse struct { + Sats int64 `json:"sats"` +} + +type AlbyPayRequest struct { + Invoice string `json:"invoice"` +} + +type AlbyMe struct { + Identifier string `json:"identifier"` + NPub string `json:"nostr_pubkey"` + LightningAddress string `json:"lightning_address"` + Email string `json:"email"` + Name string `json:"name"` + Avatar string `json:"avatar"` + KeysendPubkey string `json:"keysend_pubkey"` + SharedNode bool `json:"shared_node"` +} + +type AlbyBalance struct { + Balance int64 `json:"balance"` + Unit string `json:"unit"` + Currency string `json:"currency"` +} + +type ChannelPeerSuggestion struct { + Network string `json:"network"` + PaymentMethod string `json:"paymentMethod"` + Pubkey string `json:"pubkey"` + Host string `json:"host"` + MinimumChannelSize uint64 `json:"minimumChannelSize"` + Name string `json:"name"` + Image string `json:"image"` + Lsp string `json:"lsp"` +} + +type ErrorResponse struct { + Message string `json:"message"` +} diff --git a/api.go b/api.go deleted file mode 100644 index 02c558d6..00000000 --- a/api.go +++ /dev/null @@ -1,1552 +0,0 @@ -package main - -import ( - "archive/zip" - "bytes" - "context" - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "os" - "path/filepath" - "regexp" - "slices" - "strconv" - "strings" - "time" - - "github.com/nbd-wtf/go-nostr" - "github.com/sirupsen/logrus" - "golang.org/x/crypto/pbkdf2" - "gorm.io/gorm" - - "github.com/getAlby/nostr-wallet-connect/alby" - models "github.com/getAlby/nostr-wallet-connect/models/api" - "github.com/getAlby/nostr-wallet-connect/models/config" - "github.com/getAlby/nostr-wallet-connect/models/lnclient" - "github.com/getAlby/nostr-wallet-connect/models/lsp" - "github.com/getAlby/nostr-wallet-connect/nip47" -) - -type API struct { - svc *Service -} - -func NewAPI(svc *Service) *API { - - return &API{ - svc: svc, - } -} - -func (api *API) CreateApp(createAppRequest *models.CreateAppRequest) (*models.CreateAppResponse, error) { - name := createAppRequest.Name - var pairingPublicKey string - var pairingSecretKey string - if createAppRequest.Pubkey == "" { - pairingSecretKey = nostr.GeneratePrivateKey() - pairingPublicKey, _ = nostr.GetPublicKey(pairingSecretKey) - } else { - pairingPublicKey = createAppRequest.Pubkey - //validate public key - decoded, err := hex.DecodeString(pairingPublicKey) - if err != nil || len(decoded) != 32 { - api.svc.Logger.WithField("pairingPublicKey", pairingPublicKey).Error("Invalid public key format") - return nil, fmt.Errorf("invalid public key format: %s", pairingPublicKey) - - } - } - - app := App{Name: name, NostrPubkey: pairingPublicKey} - maxAmount := createAppRequest.MaxAmount - budgetRenewal := createAppRequest.BudgetRenewal - - expiresAt, err := api.parseExpiresAt(createAppRequest.ExpiresAt) - if err != nil { - return nil, fmt.Errorf("invalid expiresAt: %v", err) - } - - err = api.svc.db.Transaction(func(tx *gorm.DB) error { - err := tx.Save(&app).Error - if err != nil { - return err - } - - requestMethods := createAppRequest.RequestMethods - if requestMethods == "" { - return fmt.Errorf("won't create an app without request methods") - } - //request methods should be space separated list of known request kinds - methodsToCreate := strings.Split(requestMethods, " ") - for _, m := range methodsToCreate { - //if we don't know this method, we return an error - if !strings.Contains(nip47.CAPABILITIES, m) { - return fmt.Errorf("did not recognize request method: %s", m) - } - appPermission := AppPermission{ - App: app, - RequestMethod: m, - ExpiresAt: expiresAt, - //these fields are only relevant for pay_invoice - MaxAmount: maxAmount, - BudgetRenewal: budgetRenewal, - } - err = tx.Create(&appPermission).Error - if err != nil { - return err - } - } - // commit transaction - return nil - }) - - if err != nil { - return nil, err - } - - relayUrl, _ := api.svc.cfg.Get("Relay", "") - - responseBody := &models.CreateAppResponse{} - responseBody.Name = name - responseBody.Pubkey = pairingPublicKey - responseBody.PairingSecret = pairingSecretKey - - if createAppRequest.ReturnTo != "" { - returnToUrl, err := url.Parse(createAppRequest.ReturnTo) - if err == nil { - query := returnToUrl.Query() - query.Add("relay", relayUrl) - query.Add("pubkey", api.svc.cfg.NostrPublicKey) - // if user.LightningAddress != "" { - // query.Add("lud16", user.LightningAddress) - // } - returnToUrl.RawQuery = query.Encode() - responseBody.ReturnTo = returnToUrl.String() - } - } - - var lud16 string - // if user.LightningAddress != "" { - // lud16 = fmt.Sprintf("&lud16=%s", user.LightningAddress) - // } - responseBody.PairingUri = fmt.Sprintf("nostr+walletconnect://%s?relay=%s&secret=%s%s", api.svc.cfg.NostrPublicKey, relayUrl, pairingSecretKey, lud16) - return responseBody, nil -} - -func (api *API) UpdateApp(userApp *App, updateAppRequest *models.UpdateAppRequest) error { - maxAmount := updateAppRequest.MaxAmount - budgetRenewal := updateAppRequest.BudgetRenewal - - requestMethods := updateAppRequest.RequestMethods - if requestMethods == "" { - return fmt.Errorf("won't update an app to have no request methods") - } - newRequestMethods := strings.Split(requestMethods, " ") - - expiresAt, err := api.parseExpiresAt(updateAppRequest.ExpiresAt) - if err != nil { - return fmt.Errorf("invalid expiresAt: %v", err) - } - - err = api.svc.db.Transaction(func(tx *gorm.DB) error { - // Update existing permissions with new budget and expiry - err := tx.Model(&AppPermission{}).Where("app_id", userApp.ID).Updates(map[string]interface{}{ - "ExpiresAt": expiresAt, - "MaxAmount": maxAmount, - "BudgetRenewal": budgetRenewal, - }).Error - if err != nil { - return err - } - - var existingPermissions []AppPermission - if err := tx.Where("app_id = ?", userApp.ID).Find(&existingPermissions).Error; err != nil { - return err - } - - existingMethodMap := make(map[string]bool) - for _, perm := range existingPermissions { - existingMethodMap[perm.RequestMethod] = true - } - - // Add new permissions - for _, method := range newRequestMethods { - if !existingMethodMap[method] { - perm := AppPermission{ - App: *userApp, - RequestMethod: method, - ExpiresAt: expiresAt, - MaxAmount: maxAmount, - BudgetRenewal: budgetRenewal, - } - if err := tx.Create(&perm).Error; err != nil { - return err - } - } - delete(existingMethodMap, method) - } - - // Remove old permissions - for method := range existingMethodMap { - if err := tx.Where("app_id = ? AND request_method = ?", userApp.ID, method).Delete(&AppPermission{}).Error; err != nil { - return err - } - } - - // commit transaction - return nil - }) - - return err -} - -func (api *API) DeleteApp(userApp *App) error { - return api.svc.db.Delete(userApp).Error -} - -func (api *API) GetApp(userApp *App) *models.App { - - var lastEvent RequestEvent - lastEventResult := api.svc.db.Where("app_id = ?", userApp.ID).Order("id desc").Limit(1).Find(&lastEvent) - - paySpecificPermission := AppPermission{} - appPermissions := []AppPermission{} - var expiresAt *time.Time - api.svc.db.Where("app_id = ?", userApp.ID).Find(&appPermissions) - - requestMethods := []string{} - for _, appPerm := range appPermissions { - expiresAt = appPerm.ExpiresAt - if appPerm.RequestMethod == nip47.PAY_INVOICE_METHOD { - //find the pay_invoice-specific permissions - paySpecificPermission = appPerm - } - requestMethods = append(requestMethods, appPerm.RequestMethod) - } - - //renewsIn := "" - budgetUsage := int64(0) - maxAmount := paySpecificPermission.MaxAmount - if maxAmount > 0 { - budgetUsage = api.svc.GetBudgetUsage(&paySpecificPermission) - } - - response := models.App{ - Name: userApp.Name, - Description: userApp.Description, - CreatedAt: userApp.CreatedAt, - UpdatedAt: userApp.UpdatedAt, - NostrPubkey: userApp.NostrPubkey, - ExpiresAt: expiresAt, - MaxAmount: maxAmount, - RequestMethods: requestMethods, - BudgetUsage: budgetUsage, - BudgetRenewal: paySpecificPermission.BudgetRenewal, - } - - if lastEventResult.RowsAffected > 0 { - response.LastEventAt = &lastEvent.CreatedAt - } - - return &response - -} - -func (api *API) ListApps() ([]models.App, error) { - // TODO: join apps and permissions - apps := []App{} - api.svc.db.Find(&apps) - - permissions := []AppPermission{} - api.svc.db.Find(&permissions) - - permissionsMap := make(map[uint][]AppPermission) - for _, perm := range permissions { - permissionsMap[perm.AppId] = append(permissionsMap[perm.AppId], perm) - } - - apiApps := []models.App{} - for _, userApp := range apps { - apiApp := models.App{ - // ID: app.ID, - Name: userApp.Name, - Description: userApp.Description, - CreatedAt: userApp.CreatedAt, - UpdatedAt: userApp.UpdatedAt, - NostrPubkey: userApp.NostrPubkey, - } - - for _, permission := range permissionsMap[userApp.ID] { - apiApp.RequestMethods = append(apiApp.RequestMethods, permission.RequestMethod) - apiApp.ExpiresAt = permission.ExpiresAt - if permission.RequestMethod == nip47.PAY_INVOICE_METHOD { - apiApp.BudgetRenewal = permission.BudgetRenewal - apiApp.MaxAmount = permission.MaxAmount - if apiApp.MaxAmount > 0 { - apiApp.BudgetUsage = api.svc.GetBudgetUsage(&permission) - } - } - } - - var lastEvent RequestEvent - lastEventResult := api.svc.db.Where("app_id = ?", userApp.ID).Order("id desc").Limit(1).Find(&lastEvent) - if lastEventResult.RowsAffected > 0 { - apiApp.LastEventAt = &lastEvent.CreatedAt - } - - apiApps = append(apiApps, apiApp) - } - return apiApps, nil -} - -func (api *API) ListChannels(ctx context.Context) ([]lnclient.Channel, error) { - if api.svc.lnClient == nil { - return nil, errors.New("LNClient not started") - } - return api.svc.lnClient.ListChannels(ctx) -} - -func (api *API) GetChannelPeerSuggestions(ctx context.Context) ([]alby.ChannelPeerSuggestion, error) { - return api.svc.AlbyOAuthSvc.GetChannelPeerSuggestions(ctx) -} - -func (api *API) ResetRouter(key string, stopApp bool) error { - if api.svc.lnClient == nil { - return errors.New("LNClient not started") - } - err := api.svc.lnClient.ResetRouter(key) - if err != nil { - return err - } - - if stopApp { - // Because the above method has to stop the node to reset the router, - // We also need to stop the lnclient and ask the user to start it again - return api.Stop() - } - return nil -} - -func (api *API) ChangeUnlockPassword(changeUnlockPasswordRequest *models.ChangeUnlockPasswordRequest) error { - if api.svc.lnClient == nil { - return errors.New("LNClient not started") - } - - err := api.svc.cfg.ChangeUnlockPassword(changeUnlockPasswordRequest.CurrentUnlockPassword, changeUnlockPasswordRequest.NewUnlockPassword) - - if err != nil { - api.svc.Logger.WithError(err).Error("failed to change unlock password") - return err - } - - // Because all the encrypted fields have changed - // we also need to stop the lnclient and ask the user to start it again - return api.Stop() -} - -func (api *API) Stop() error { - api.svc.Logger.Info("Running Stop command") - if api.svc.lnClient == nil { - return errors.New("LNClient not started") - } - // stop the lnclient - // The user will be forced to re-enter their unlock password to restart the node - err := api.svc.StopLNClient() - if err != nil { - api.svc.Logger.WithError(err).Error("Failed to stop LNClient") - } - return err -} - -func (api *API) GetNodeConnectionInfo(ctx context.Context) (*lnclient.NodeConnectionInfo, error) { - if api.svc.lnClient == nil { - return nil, errors.New("LNClient not started") - } - return api.svc.lnClient.GetNodeConnectionInfo(ctx) -} - -func (api *API) GetNodeStatus(ctx context.Context) (*lnclient.NodeStatus, error) { - if api.svc.lnClient == nil { - return nil, errors.New("LNClient not started") - } - return api.svc.lnClient.GetNodeStatus(ctx) -} - -func (api *API) ListPeers(ctx context.Context) ([]lnclient.PeerDetails, error) { - if api.svc.lnClient == nil { - return nil, errors.New("LNClient not started") - } - return api.svc.lnClient.ListPeers(ctx) -} - -func (api *API) ConnectPeer(ctx context.Context, connectPeerRequest *models.ConnectPeerRequest) error { - if api.svc.lnClient == nil { - return errors.New("LNClient not started") - } - return api.svc.lnClient.ConnectPeer(ctx, connectPeerRequest) -} - -func (api *API) OpenChannel(ctx context.Context, openChannelRequest *models.OpenChannelRequest) (*models.OpenChannelResponse, error) { - if api.svc.lnClient == nil { - return nil, errors.New("LNClient not started") - } - return api.svc.lnClient.OpenChannel(ctx, openChannelRequest) -} - -func (api *API) CloseChannel(ctx context.Context, peerId, channelId string, force bool) (*models.CloseChannelResponse, error) { - if api.svc.lnClient == nil { - return nil, errors.New("LNClient not started") - } - api.svc.Logger.WithFields(logrus.Fields{ - "peer_id": peerId, - "channel_id": channelId, - "force": force, - }).Info("Closing channel") - return api.svc.lnClient.CloseChannel(ctx, &lnclient.CloseChannelRequest{ - NodeId: peerId, - ChannelId: channelId, - Force: force, - }) -} - -func (api *API) GetNewOnchainAddress(ctx context.Context) (*models.NewOnchainAddressResponse, error) { - if api.svc.lnClient == nil { - return nil, errors.New("LNClient not started") - } - address, err := api.svc.lnClient.GetNewOnchainAddress(ctx) - if err != nil { - return nil, err - } - return &models.NewOnchainAddressResponse{ - Address: address, - }, nil -} - -func (api *API) SignMessage(ctx context.Context, message string) (*models.SignMessageResponse, error) { - if api.svc.lnClient == nil { - return nil, errors.New("LNClient not started") - } - signature, err := api.svc.lnClient.SignMessage(ctx, message) - if err != nil { - return nil, err - } - return &models.SignMessageResponse{ - Message: message, - Signature: signature, - }, nil -} - -func (api *API) RedeemOnchainFunds(ctx context.Context, toAddress string) (*models.RedeemOnchainFundsResponse, error) { - if api.svc.lnClient == nil { - return nil, errors.New("LNClient not started") - } - txId, err := api.svc.lnClient.RedeemOnchainFunds(ctx, toAddress) - if err != nil { - return nil, err - } - return &models.RedeemOnchainFundsResponse{ - TxId: txId, - }, nil -} - -func (api *API) GetBalances(ctx context.Context) (*models.BalancesResponse, error) { - if api.svc.lnClient == nil { - return nil, errors.New("LNClient not started") - } - balances, err := api.svc.lnClient.GetBalances(ctx) - if err != nil { - return nil, err - } - return balances, nil -} - -func (api *API) RequestMempoolApi(endpoint string) (interface{}, error) { - url := api.svc.cfg.Env.MempoolApi + endpoint - - client := http.Client{ - Timeout: time.Second * 10, - } - - req, err := http.NewRequest(http.MethodGet, url, nil) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": url, - }).Error("Failed to create http request") - return nil, err - } - - res, err := client.Do(req) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": url, - }).Error("Failed to send request") - return nil, err - } - - defer res.Body.Close() - - body, readErr := io.ReadAll(res.Body) - if readErr != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": url, - }).Error("Failed to read response body") - return nil, errors.New("failed to read response body") - } - - var jsonContent interface{} - jsonErr := json.Unmarshal(body, &jsonContent) - if jsonErr != nil { - api.svc.Logger.WithError(jsonErr).WithFields(logrus.Fields{ - "url": url, - }).Error("Failed to deserialize json") - return nil, fmt.Errorf("failed to deserialize json %s %s", url, string(body)) - } - return jsonContent, nil -} - -func (api *API) NewInstantChannelInvoice(ctx context.Context, request *models.NewInstantChannelInvoiceRequest) (*models.NewInstantChannelInvoiceResponse, error) { - var selectedLsp lsp.LSP - switch request.LSP { - case "VOLTAGE": - selectedLsp = lsp.VoltageLSP() - case "OLYMPUS_FLOW_2_0": - selectedLsp = lsp.OlympusLSP() - case "OLYMPUS_MUTINYNET_FLOW_2_0": - selectedLsp = lsp.OlympusMutinynetFlowLSP() - case "OLYMPUS_MUTINYNET_LSPS1": - selectedLsp = lsp.OlympusMutinynetLSPS1LSP() - case "ALBY": - selectedLsp = lsp.AlbyPlebsLSP() - case "ALBY_MUTINYNET": - selectedLsp = lsp.AlbyMutinynetPlebsLSP() - default: - return nil, errors.New("unknown LSP") - } - - if api.svc.lnClient == nil { - return nil, errors.New("LNClient not started") - } - - api.svc.Logger.Infoln("Requesting LSP info") - - var lspInfo *LSPInfo - var err error - switch selectedLsp.LspType { - case lsp.LSP_TYPE_FLOW_2_0: - fallthrough - case lsp.LSP_TYPE_PMLSP: - lspInfo, err = api.getFlowLSPInfo(selectedLsp.Url + "/info") - - case lsp.LSP_TYPE_LSPS1: - lspInfo, err = api.getLSPS1LSPInfo(selectedLsp.Url + "/get_info") - - default: - return nil, fmt.Errorf("unsupported LSP type: %v", selectedLsp.LspType) - } - if err != nil { - api.svc.Logger.WithError(err).Error("Failed to request LSP info") - return nil, err - } - - api.svc.Logger.Infoln("Requesting own node info") - - nodeInfo, err := api.svc.lnClient.GetInfo(ctx) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": selectedLsp.Url, - }).Error("Failed to request own node info", err) - return nil, err - } - - api.svc.Logger.WithField("lspInfo", lspInfo).Info("Connecting to LSP node as a peer") - - err = api.ConnectPeer(ctx, &models.ConnectPeerRequest{ - Pubkey: lspInfo.Pubkey, - Address: lspInfo.Address, - Port: lspInfo.Port, - }) - - if err != nil { - api.svc.Logger.WithError(err).Error("Failed to connect to peer") - return nil, err - } - - invoice := "" - var fee uint64 = 0 - - switch selectedLsp.LspType { - case lsp.LSP_TYPE_FLOW_2_0: - invoice, fee, err = api.requestFlow20WrappedInvoice(ctx, &selectedLsp, request.Amount, nodeInfo.Pubkey) - case lsp.LSP_TYPE_PMLSP: - invoice, fee, err = api.requestPMLSPInvoice(&selectedLsp, request.Amount, nodeInfo.Pubkey) - case lsp.LSP_TYPE_LSPS1: - invoice, fee, err = api.requestLSPS1Invoice(&selectedLsp, request.Amount, nodeInfo.Pubkey) - - default: - return nil, fmt.Errorf("unsupported LSP type: %v", selectedLsp.LspType) - } - if err != nil { - api.svc.Logger.WithError(err).Error("Failed to request invoice") - return nil, err - } - - newChannelResponse := &models.NewInstantChannelInvoiceResponse{ - Invoice: invoice, - Fee: fee, - } - - api.svc.Logger.WithFields(logrus.Fields{ - "newChannelResponse": newChannelResponse, - }).Info("New Channel response") - - return newChannelResponse, nil -} - -// TODO: move to LSP file -type LSPInfo struct { - Pubkey string - Address string - Port uint16 -} - -// TODO: move to LSP file -func (api *API) getLSPS1LSPInfo(url string) (*LSPInfo, error) { - type LSPS1LSPInfo struct { - // TODO: implement options - Options interface{} `json:"options"` - URIs []string `json:"uris"` - } - var lsps1LspInfo LSPS1LSPInfo - client := http.Client{ - Timeout: time.Second * 10, - } - req, err := http.NewRequest(http.MethodGet, url, nil) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": url, - }).Error("Failed to create lsp info request") - return nil, err - } - - res, err := client.Do(req) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": url, - }).Error("Failed to request lsp info") - return nil, err - } - - defer res.Body.Close() - - body, err := io.ReadAll(res.Body) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": url, - }).Error("Failed to read response body") - return nil, errors.New("failed to read response body") - } - - err = json.Unmarshal(body, &lsps1LspInfo) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": url, - }).Error("Failed to deserialize json") - return nil, fmt.Errorf("failed to deserialize json %s %s", url, string(body)) - } - - uri := lsps1LspInfo.URIs[0] - - // make sure it's a valid IPv4 URI - regex := regexp.MustCompile(`^([0-9a-f]+)@([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+):([0-9]+)$`) - parts := regex.FindStringSubmatch(uri) - api.svc.Logger.WithField("parts", parts).Info("Split URI") - if parts == nil || len(parts) != 4 { - api.svc.Logger.WithField("parts", parts).Info("Unsupported URI") - return nil, errors.New("could not decode LSP URI") - } - - port, err := strconv.Atoi(parts[3]) - if err != nil { - api.svc.Logger.WithField("port", parts[3]).WithError(err).Info("Failed to decode port number") - - return nil, err - } - - return &LSPInfo{ - Pubkey: parts[1], - Address: parts[2], - Port: uint16(port), - }, nil -} -func (api *API) getFlowLSPInfo(url string) (*LSPInfo, error) { - type FlowLSPConnectionMethod struct { - Address string `json:"address"` - Port uint16 `json:"port"` - Type string `json:"type"` - } - type FlowLSPInfo struct { - Pubkey string `json:"pubkey"` - ConnectionMethods []FlowLSPConnectionMethod `json:"connection_methods"` - } - var flowLspInfo FlowLSPInfo - client := http.Client{ - Timeout: time.Second * 10, - } - req, err := http.NewRequest(http.MethodGet, url, nil) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": url, - }).Error("Failed to create lsp info request") - return nil, err - } - - res, err := client.Do(req) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": url, - }).Error("Failed to request lsp info") - return nil, err - } - - defer res.Body.Close() - - body, err := io.ReadAll(res.Body) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": url, - }).Error("Failed to read response body") - return nil, errors.New("failed to read response body") - } - - err = json.Unmarshal(body, &flowLspInfo) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": url, - }).Error("Failed to deserialize json") - return nil, fmt.Errorf("failed to deserialize json %s %s", url, string(body)) - } - - ipIndex := -1 - for i, cm := range flowLspInfo.ConnectionMethods { - if strings.HasPrefix(cm.Type, "ip") { - ipIndex = i - break - } - } - - if ipIndex == -1 { - api.svc.Logger.Error("No ipv4/ipv6 connection method found in LSP info") - return nil, errors.New("unexpected LSP connection method") - } - - return &LSPInfo{ - Pubkey: flowLspInfo.Pubkey, - Address: flowLspInfo.ConnectionMethods[ipIndex].Address, - Port: flowLspInfo.ConnectionMethods[ipIndex].Port, - }, nil -} - -// TODO: move to LSP file -func (api *API) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *lsp.LSP, amount uint64, pubkey string) (invoice string, fee uint64, err error) { - api.svc.Logger.Infoln("Requesting fee information") - - type FeeRequest struct { - AmountMsat uint64 `json:"amount_msat"` - Pubkey string `json:"pubkey"` - } - type FeeResponse struct { - FeeAmountMsat uint64 `json:"fee_amount_msat"` - Id string `json:"id"` - } - - var feeResponse FeeResponse - { - client := http.Client{ - Timeout: time.Second * 10, - } - payloadBytes, err := json.Marshal(FeeRequest{ - AmountMsat: amount * 1000, - Pubkey: pubkey, - }) - if err != nil { - return "", 0, err - } - bodyReader := bytes.NewReader(payloadBytes) - - req, err := http.NewRequest(http.MethodPost, selectedLsp.Url+"/fee", bodyReader) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": selectedLsp.Url, - }).Error("Failed to create lsp fee request") - return "", 0, err - } - - req.Header.Set("Content-Type", "application/json") - - res, err := client.Do(req) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": selectedLsp.Url, - }).Error("Failed to request lsp fee") - return "", 0, err - } - - defer res.Body.Close() - - body, err := io.ReadAll(res.Body) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": selectedLsp.Url, - }).Error("Failed to read response body") - return "", 0, errors.New("failed to read response body") - } - - if res.StatusCode >= 300 { - api.svc.Logger.WithFields(logrus.Fields{ - "body": string(body), - "statusCode": res.StatusCode, - }).Error("fee endpoint returned non-success code") - return "", 0, fmt.Errorf("fee endpoint returned non-success code: %s", string(body)) - } - - err = json.Unmarshal(body, &feeResponse) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": selectedLsp.Url, - }).Error("Failed to deserialize json") - return "", 0, fmt.Errorf("failed to deserialize json %s %s", selectedLsp.Url, string(body)) - } - - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": selectedLsp.Url, - "feeResponse": feeResponse, - }).Info("Got fee response") - if feeResponse.Id == "" { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "feeResponse": feeResponse, - }).Error("No fee id in fee response") - return "", 0, fmt.Errorf("no fee id in fee response %v", feeResponse) - } - fee = feeResponse.FeeAmountMsat / 1000 - } - - // because we don't want the sender to pay the fee - // see: https://docs.voltage.cloud/voltage-lsp#gqBqV - makeInvoiceResponse, err := api.svc.lnClient.MakeInvoice(ctx, int64(amount)*1000-int64(feeResponse.FeeAmountMsat), "", "", 60*60) - if err != nil { - api.svc.Logger.WithError(err).Error("Failed to request own invoice") - return "", 0, fmt.Errorf("failed to request own invoice %v", err) - } - - type ProposalRequest struct { - Bolt11 string `json:"bolt11"` - FeeId string `json:"fee_id"` - } - type ProposalResponse struct { - Bolt11 string `json:"jit_bolt11"` - } - - api.svc.Logger.Infoln("Proposing invoice") - - var proposalResponse ProposalResponse - { - client := http.Client{ - Timeout: time.Second * 10, - } - payloadBytes, err := json.Marshal(ProposalRequest{ - Bolt11: makeInvoiceResponse.Invoice, - FeeId: feeResponse.Id, - }) - if err != nil { - return "", 0, err - } - bodyReader := bytes.NewReader(payloadBytes) - - req, err := http.NewRequest(http.MethodPost, selectedLsp.Url+"/proposal", bodyReader) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": selectedLsp.Url, - }).Error("Failed to create lsp fee request") - return "", 0, err - } - - req.Header.Set("Content-Type", "application/json") - - res, err := client.Do(req) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": selectedLsp.Url, - }).Error("Failed to request lsp fee") - return "", 0, err - } - - defer res.Body.Close() - - body, err := io.ReadAll(res.Body) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": selectedLsp.Url, - }).Error("Failed to read response body") - return "", 0, errors.New("failed to read response body") - } - - if res.StatusCode >= 300 { - api.svc.Logger.WithFields(logrus.Fields{ - "body": string(body), - "statusCode": res.StatusCode, - }).Error("proposal endpoint returned non-success code") - return "", 0, fmt.Errorf("proposal endpoint returned non-success code: %s", string(body)) - } - - err = json.Unmarshal(body, &proposalResponse) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": selectedLsp.Url, - }).Error("Failed to deserialize json") - return "", 0, fmt.Errorf("failed to deserialize json %s %s", selectedLsp.Url, string(body)) - } - api.svc.Logger.WithField("proposalResponse", proposalResponse).Info("Got proposal response") - if proposalResponse.Bolt11 == "" { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": selectedLsp.Url, - "proposalResponse": proposalResponse, - }).Error("No bolt11 in proposal response") - return "", 0, fmt.Errorf("no bolt11 in proposal response %v", proposalResponse) - } - } - invoice = proposalResponse.Bolt11 - - return invoice, fee, nil -} - -func (api *API) requestPMLSPInvoice(selectedLsp *lsp.LSP, amount uint64, pubkey string) (invoice string, fee uint64, err error) { - type NewInstantChannelRequest struct { - Amount uint64 `json:"amount"` - Pubkey string `json:"pubkey"` - } - - client := http.Client{ - Timeout: time.Second * 10, - } - payloadBytes, err := json.Marshal(NewInstantChannelRequest{ - Amount: amount, - Pubkey: pubkey, - }) - if err != nil { - return "", 0, err - } - bodyReader := bytes.NewReader(payloadBytes) - - req, err := http.NewRequest(http.MethodPost, selectedLsp.Url+"/new-channel", bodyReader) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": selectedLsp.Url, - }).Error("Failed to create new channel request") - return "", 0, err - } - - req.Header.Set("Content-Type", "application/json") - - res, err := client.Do(req) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": selectedLsp.Url, - }).Error("Failed to request new channel invoice") - return "", 0, err - } - - defer res.Body.Close() - - body, err := io.ReadAll(res.Body) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": selectedLsp.Url, - }).Error("Failed to read response body") - return "", 0, errors.New("failed to read response body") - } - - if res.StatusCode >= 300 { - api.svc.Logger.WithFields(logrus.Fields{ - "body": string(body), - "statusCode": res.StatusCode, - }).Error("new-channel endpoint returned non-success code") - return "", 0, fmt.Errorf("new-channel endpoint returned non-success code: %s", string(body)) - } - - var newChannelResponse lsp.NewInstantChannelResponse - - err = json.Unmarshal(body, &newChannelResponse) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": selectedLsp.Url, - }).Error("Failed to deserialize json") - return "", 0, fmt.Errorf("failed to deserialize json %s %s", selectedLsp.Url, string(body)) - } - - invoice = newChannelResponse.Invoice - fee = newChannelResponse.FeeAmountMsat / 1000 - - return invoice, fee, nil -} - -func (api *API) requestLSPS1Invoice(selectedLsp *lsp.LSP, amount uint64, pubkey string) (invoice string, fee uint64, err error) { - client := http.Client{ - Timeout: time.Second * 10, - } - - type NewLSPS1ChannelRequest struct { - PublicKey string `json:"public_key"` - LSPBalanceSat string `json:"lsp_balance_sat"` - ClientBalanceSat string `json:"client_balance_sat"` - RequiredChannelConfirmations uint64 `json:"required_channel_confirmations"` - FundingConfirmsWithinBlocks uint64 `json:"funding_confirms_within_blocks"` - ChannelExpiryBlocks uint64 `json:"channel_expiry_blocks"` - Token string `json:"token"` - RefundOnchainAddress string `json:"refund_onchain_address"` - AnnounceChannel bool `json:"announce_channel"` - } - - newLSPS1ChannelRequest := NewLSPS1ChannelRequest{ - PublicKey: pubkey, - LSPBalanceSat: strconv.FormatUint(amount, 10), - ClientBalanceSat: "0", - RequiredChannelConfirmations: 0, - FundingConfirmsWithinBlocks: 6, - ChannelExpiryBlocks: 13000, // TODO: do not hardcode this - Token: "", - RefundOnchainAddress: "tb1qtz33lpvxl543f4glfe3pecpznn4cked0yhvg78", // TODO: get from node - AnnounceChannel: false, // TODO: this should be customizable - } - - payloadBytes, err := json.Marshal(newLSPS1ChannelRequest) - if err != nil { - return "", 0, err - } - bodyReader := bytes.NewReader(payloadBytes) - - req, err := http.NewRequest(http.MethodPost, selectedLsp.Url+"/create_order", bodyReader) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": selectedLsp.Url, - }).Error("Failed to create new channel request") - return "", 0, err - } - - req.Header.Set("Content-Type", "application/json") - - res, err := client.Do(req) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": selectedLsp.Url, - }).Error("Failed to request new channel invoice") - return "", 0, err - } - - defer res.Body.Close() - - body, err := io.ReadAll(res.Body) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": selectedLsp.Url, - }).Error("Failed to read response body") - return "", 0, errors.New("failed to read response body") - } - - if res.StatusCode >= 300 { - api.svc.Logger.WithFields(logrus.Fields{ - "newLSPS1ChannelRequest": newLSPS1ChannelRequest, - "body": string(body), - "statusCode": res.StatusCode, - }).Error("create_order endpoint returned non-success code") - return "", 0, fmt.Errorf("create_order endpoint returned non-success code: %s", string(body)) - } - - type NewLSPS1ChannelPayment struct { - LightningInvoice string `json:"lightning_invoice"` - FeeTotalSat string `json:"fee_total_sat"` - } - type NewLSPS1ChannelResponse struct { - Payment NewLSPS1ChannelPayment `json:"payment"` - } - - var newChannelResponse NewLSPS1ChannelResponse - - err = json.Unmarshal(body, &newChannelResponse) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": selectedLsp.Url, - }).Error("Failed to deserialize json") - return "", 0, fmt.Errorf("failed to deserialize json %s %s", selectedLsp.Url, string(body)) - } - - invoice = newChannelResponse.Payment.LightningInvoice - fee, err = strconv.ParseUint(newChannelResponse.Payment.FeeTotalSat, 10, 64) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "url": selectedLsp.Url, - }).Error("Failed to parse fee") - return "", 0, fmt.Errorf("failed to parse fee %v", err) - } - - return invoice, fee, nil -} - -func (api *API) GetInfo(ctx context.Context) (*models.InfoResponse, error) { - info := models.InfoResponse{} - backendType, _ := api.svc.cfg.Get("LNBackendType", "") - unlockPasswordCheck, _ := api.svc.cfg.Get("UnlockPasswordCheck", "") - info.SetupCompleted = unlockPasswordCheck != "" - info.Running = api.svc.lnClient != nil - info.BackendType = backendType - info.AlbyAuthUrl = api.svc.AlbyOAuthSvc.GetAuthUrl() - info.OAuthRedirect = !api.svc.cfg.Env.IsDefaultClientId() - albyUserIdentifier, err := api.svc.AlbyOAuthSvc.GetUserIdentifier() - if err != nil { - api.svc.Logger.WithError(err).Error("Failed to get alby user identifier") - return nil, err - } - info.AlbyUserIdentifier = albyUserIdentifier - info.AlbyAccountConnected = api.svc.AlbyOAuthSvc.IsConnected(ctx) - if api.svc.lnClient != nil { - // TODO: is there a better way to do this? - if backendType == config.BreezBackendType { - info.OnboardingCompleted = true - } else { - channels, err := api.ListChannels(ctx) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{}).Error("Failed to fetch channels") - return nil, err - } - info.OnboardingCompleted = len(channels) > 0 - } - - nodeInfo, err := api.svc.lnClient.GetInfo(ctx) - if err != nil { - api.svc.Logger.WithError(err).Error("Failed to get nodeInfo") - return nil, err - } - - info.Network = nodeInfo.Network - } - - if info.BackendType != config.LNDBackendType { - nextBackupReminder, _ := api.svc.cfg.Get("NextBackupReminder", "") - var err error - parsedTime := time.Time{} - if nextBackupReminder != "" { - parsedTime, err = time.Parse(time.RFC3339, nextBackupReminder) - if err != nil { - api.svc.Logger.WithError(err).WithFields(logrus.Fields{ - "nextBackupReminder": nextBackupReminder, - }).Error("Error parsing time") - return nil, err - } - } - info.ShowBackupReminder = parsedTime.IsZero() || parsedTime.Before(time.Now()) - } - - return &info, nil -} - -func (api *API) GetEncryptedMnemonic() *models.EncryptedMnemonicResponse { - resp := models.EncryptedMnemonicResponse{} - mnemonic, _ := api.svc.cfg.Get("Mnemonic", "") - resp.Mnemonic = mnemonic - return &resp -} - -func (api *API) SetNextBackupReminder(backupReminderRequest *models.BackupReminderRequest) error { - api.svc.cfg.SetUpdate("NextBackupReminder", backupReminderRequest.NextBackupReminder, "") - return nil -} - -func (api *API) Start(startRequest *models.StartRequest) error { - return api.svc.StartApp(startRequest.UnlockPassword) -} - -func (api *API) Setup(ctx context.Context, setupRequest *models.SetupRequest) error { - info, err := api.GetInfo(ctx) - if err != nil { - api.svc.Logger.WithError(err).Error("Failed to get info") - return err - } - if info.SetupCompleted { - api.svc.Logger.Error("Cannot re-setup node") - return errors.New("setup already completed") - } - - api.svc.cfg.SavePasswordCheck(setupRequest.UnlockPassword) - - // update next backup reminder - api.svc.cfg.SetUpdate("NextBackupReminder", setupRequest.NextBackupReminder, "") - // only update non-empty values - if setupRequest.LNBackendType != "" { - api.svc.cfg.SetUpdate("LNBackendType", setupRequest.LNBackendType, "") - } - if setupRequest.BreezAPIKey != "" { - api.svc.cfg.SetUpdate("BreezAPIKey", setupRequest.BreezAPIKey, setupRequest.UnlockPassword) - } - if setupRequest.Mnemonic != "" { - api.svc.cfg.SetUpdate("Mnemonic", setupRequest.Mnemonic, setupRequest.UnlockPassword) - } - if setupRequest.GreenlightInviteCode != "" { - api.svc.cfg.SetUpdate("GreenlightInviteCode", setupRequest.GreenlightInviteCode, setupRequest.UnlockPassword) - } - if setupRequest.LNDAddress != "" { - api.svc.cfg.SetUpdate("LNDAddress", setupRequest.LNDAddress, setupRequest.UnlockPassword) - } - if setupRequest.LNDCertHex != "" { - api.svc.cfg.SetUpdate("LNDCertHex", setupRequest.LNDCertHex, setupRequest.UnlockPassword) - } - if setupRequest.LNDMacaroonHex != "" { - api.svc.cfg.SetUpdate("LNDMacaroonHex", setupRequest.LNDMacaroonHex, setupRequest.UnlockPassword) - } - - return nil -} - -func (api *API) SendPaymentProbes(ctx context.Context, sendPaymentProbesRequest *models.SendPaymentProbesRequest) (*models.SendPaymentProbesResponse, error) { - if api.svc.lnClient == nil { - return nil, errors.New("LNClient not started") - } - - var errMessage string - err := api.svc.lnClient.SendPaymentProbes(ctx, sendPaymentProbesRequest.Invoice) - if err != nil { - errMessage = err.Error() - } - - return &models.SendPaymentProbesResponse{Error: errMessage}, nil -} - -func (api *API) SendSpontaneousPaymentProbes(ctx context.Context, sendSpontaneousPaymentProbesRequest *models.SendSpontaneousPaymentProbesRequest) (*models.SendSpontaneousPaymentProbesResponse, error) { - if api.svc.lnClient == nil { - return nil, errors.New("LNClient not started") - } - - var errMessage string - err := api.svc.lnClient.SendSpontaneousPaymentProbes(ctx, sendSpontaneousPaymentProbesRequest.Amount, sendSpontaneousPaymentProbesRequest.NodeId) - if err != nil { - errMessage = err.Error() - } - - return &models.SendSpontaneousPaymentProbesResponse{Error: errMessage}, nil -} - -func (api *API) GetNetworkGraph(nodeIds []string) (models.NetworkGraphResponse, error) { - if api.svc.lnClient == nil { - return nil, errors.New("LNClient not started") - } - return api.svc.lnClient.GetNetworkGraph(nodeIds) -} - -func (api *API) SyncWallet() { - api.svc.lastWalletSyncRequest = time.Now() -} - -func (api *API) GetLogOutput(ctx context.Context, logType string, getLogRequest *models.GetLogOutputRequest) (*models.GetLogOutputResponse, error) { - var err error - var logData []byte - - if logType == models.LogTypeNode { - if api.svc.lnClient == nil { - return nil, errors.New("LNClient not started") - } - - logData, err = api.svc.lnClient.GetLogOutput(ctx, getLogRequest.MaxLen) - if err != nil { - return nil, err - } - } else if logType == models.LogTypeApp { - logFileName := api.svc.LogFilePath() - - logData, err = ReadFileTail(logFileName, getLogRequest.MaxLen) - if err != nil { - return nil, err - } - } else { - return nil, fmt.Errorf("invalid log type: '%s'", logType) - } - - return &models.GetLogOutputResponse{Log: string(logData)}, nil -} - -func (api *API) CreateBackup(basicBackupRequest *models.BasicBackupRequest, w io.Writer) error { - var err error - - if !api.svc.cfg.CheckUnlockPassword(basicBackupRequest.UnlockPassword) { - return errors.New("invalid unlock password") - } - - workDir, err := filepath.Abs(api.svc.cfg.Env.Workdir) - if err != nil { - return fmt.Errorf("failed to get absolute workdir: %w", err) - } - - lnStorageDir := "" - - if api.svc.lnClient != nil { - lnStorageDir, err = api.svc.lnClient.GetStorageDir() - if err != nil { - return fmt.Errorf("failed to get storage dir: %w", err) - } - api.svc.Logger.WithField("path", lnStorageDir).Info("Found node storage dir") - } - - // Reset the routing data to decrease the LDK DB size - api.ResetRouter("ALL", false) - // Stop the app to ensure no new requests are processed. - api.svc.StopApp() - db, err := api.svc.db.DB() - if err != nil { - return fmt.Errorf("failed to get database connection: %w", err) - } - - // Closing the database leaves the service in an inconsistent state, - // but that should not be a problem since the app is not expected - // to be used after its data is exported. - err = db.Close() - if err != nil { - return fmt.Errorf("failed to close database connection: %w", err) - } - - var filesToArchive []string - - if lnStorageDir != "" { - lnFiles, err := filepath.Glob(filepath.Join(workDir, lnStorageDir, "*")) - if err != nil { - return fmt.Errorf("failed to list files in the LNClient storage directory: %w", err) - } - api.svc.Logger.WithField("lnFiles", lnFiles).Info("Listed node storage dir") - - // Avoid backing up log files. - slices.DeleteFunc(lnFiles, func(s string) bool { - return filepath.Ext(s) == ".log" - }) - - filesToArchive = append(filesToArchive, lnFiles...) - } - - cw, err := encryptingWriter(w, basicBackupRequest.UnlockPassword) - if err != nil { - return fmt.Errorf("failed to create encrypted writer: %w", err) - } - - zw := zip.NewWriter(cw) - defer zw.Close() - - addFileToZip := func(fsPath, zipPath string) error { - inF, err := os.Open(fsPath) - if err != nil { - return fmt.Errorf("failed to open source file for reading: %w", err) - } - defer inF.Close() - - outW, err := zw.Create(zipPath) - if err != nil { - return fmt.Errorf("failed to create zip entry: %w", err) - } - - _, err = io.Copy(outW, inF) - return err - } - - // Locate the main database file. - dbFilePath := api.svc.cfg.Env.DatabaseUri - // Add the database file to the archive. - api.svc.Logger.WithField("nwc.db", dbFilePath).Info("adding nwc db to zip") - err = addFileToZip(dbFilePath, "nwc.db") - if err != nil { - api.svc.Logger.WithError(err).Error("Failed to zip nwc db") - return fmt.Errorf("failed to write nwc db file to zip: %w", err) - } - - for _, fileToArchive := range filesToArchive { - api.svc.Logger.WithField("fileToArchive", fileToArchive).Info("adding file to zip") - relPath, err := filepath.Rel(workDir, fileToArchive) - if err != nil { - api.svc.Logger.WithError(err).Error("Failed to get relative path of input file") - return fmt.Errorf("failed to get relative path of input file: %w", err) - } - - // Ensure forward slashes for zip format compatibility. - err = addFileToZip(fileToArchive, filepath.ToSlash(relPath)) - if err != nil { - api.svc.Logger.WithError(err).Error("Failed to write file to zip") - return fmt.Errorf("failed to write input file to zip: %w", err) - } - } - - return nil -} - -func (api *API) RestoreBackup(password string, r io.Reader) error { - workDir, err := filepath.Abs(api.svc.cfg.Env.Workdir) - if err != nil { - return fmt.Errorf("failed to get absolute workdir: %w", err) - } - - if strings.HasPrefix(api.svc.cfg.Env.DatabaseUri, "file:") { - return errors.New("cannot restore backup when database path is a file URI") - } - - cr, err := decryptingReader(r, password) - if err != nil { - return fmt.Errorf("failed to create decrypted reader: %w", err) - } - - tmpF, err := os.CreateTemp("", "nwc-*.bkp") - if err != nil { - return fmt.Errorf("failed to create temporary output file: %w", err) - } - tmpName := tmpF.Name() - defer os.Remove(tmpName) - defer tmpF.Close() - - zipSize, err := io.Copy(tmpF, cr) - if err != nil { - return fmt.Errorf("failed to decrypt backup data into temporary file: %w", err) - } - - if err = tmpF.Sync(); err != nil { - return fmt.Errorf("failed to flush temporary file: %w", err) - } - - if _, err = tmpF.Seek(0, 0); err != nil { - return fmt.Errorf("failed to seek to beginning of temporary file: %w", err) - } - - zr, err := zip.NewReader(tmpF, zipSize) - if err != nil { - return fmt.Errorf("failed to create zip reader: %w", err) - } - - extractZipEntry := func(zipFile *zip.File) error { - fsFilePath := filepath.Join(workDir, "restore", filepath.FromSlash(zipFile.Name)) - - if err = os.MkdirAll(filepath.Dir(fsFilePath), 0700); err != nil { - return fmt.Errorf("failed to create directory for zip entry: %w", err) - } - - inF, err := zipFile.Open() - if err != nil { - return fmt.Errorf("failed to open zip entry for reading: %w", err) - } - defer inF.Close() - - outF, err := os.OpenFile(fsFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) - if err != nil { - return fmt.Errorf("failed to create destination file: %w", err) - } - defer outF.Close() - - if _, err = io.Copy(outF, inF); err != nil { - return fmt.Errorf("failed to write zip entry to destination file: %w", err) - } - - return nil - } - - api.svc.Logger.WithField("count", len(zr.File)).Info("Extracting files") - for _, f := range zr.File { - api.svc.Logger.WithField("file", f.Name).Info("Extracting file") - if err = extractZipEntry(f); err != nil { - return fmt.Errorf("failed to extract zip entry: %w", err) - } - } - api.svc.Logger.WithField("count", len(zr.File)).Info("Extracted files") - - go func() { - api.svc.Logger.Info("Backup restored. Shutting down Alby Hub...") - // schedule node shutdown after a few seconds to ensure frontend updates - time.Sleep(5 * time.Second) - os.Exit(0) - }() - - return nil -} - -func (api *API) parseExpiresAt(expiresAtString string) (*time.Time, error) { - var expiresAt *time.Time - if expiresAtString != "" { - var err error - expiresAtValue, err := time.Parse(time.RFC3339, expiresAtString) - if err != nil { - api.svc.Logger.WithField("expiresAt", expiresAtString).Error("Invalid expiresAt") - return nil, fmt.Errorf("invalid expiresAt: %v", err) - } - expiresAtValue = time.Date(expiresAtValue.Year(), expiresAtValue.Month(), expiresAtValue.Day(), 23, 59, 59, 0, expiresAtValue.Location()) - expiresAt = &expiresAtValue - } - return expiresAt, nil -} - -func encryptingWriter(w io.Writer, password string) (io.Writer, error) { - salt := make([]byte, 8) - if _, err := rand.Read(salt); err != nil { - return nil, fmt.Errorf("failed to generate salt: %w", err) - } - - encKey := pbkdf2.Key([]byte(password), salt, 4096, 32, sha256.New) - block, err := aes.NewCipher(encKey) - if err != nil { - return nil, fmt.Errorf("failed to create AES cipher: %w", err) - } - - iv := make([]byte, aes.BlockSize) - if _, err = rand.Read(iv); err != nil { - return nil, fmt.Errorf("failed to generate IV: %w", err) - } - - _, err = w.Write(salt) - if err != nil { - return nil, fmt.Errorf("failed to write salt: %w", err) - } - - _, err = w.Write(iv) - if err != nil { - return nil, fmt.Errorf("failed to write IV: %w", err) - } - - stream := cipher.NewOFB(block, iv) - cw := &cipher.StreamWriter{ - S: stream, - W: w, - } - - return cw, nil -} - -func decryptingReader(r io.Reader, password string) (io.Reader, error) { - salt := make([]byte, 8) - if _, err := io.ReadFull(r, salt); err != nil { - return nil, fmt.Errorf("failed to read salt: %w", err) - } - - iv := make([]byte, aes.BlockSize) - if _, err := io.ReadFull(r, iv); err != nil { - return nil, fmt.Errorf("failed to read IV: %w", err) - } - - encKey := pbkdf2.Key([]byte(password), salt, 4096, 32, sha256.New) - block, err := aes.NewCipher(encKey) - if err != nil { - return nil, fmt.Errorf("failed to create AES cipher: %w", err) - } - - stream := cipher.NewOFB(block, iv) - cr := &cipher.StreamReader{ - S: stream, - R: r, - } - - return cr, nil -} diff --git a/api/api.go b/api/api.go new file mode 100644 index 00000000..2043bd63 --- /dev/null +++ b/api/api.go @@ -0,0 +1,643 @@ +package api + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/sirupsen/logrus" + "gorm.io/gorm" + + "github.com/getAlby/nostr-wallet-connect/alby" + "github.com/getAlby/nostr-wallet-connect/backup" + "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/lsp" + "github.com/getAlby/nostr-wallet-connect/nip47" + "github.com/getAlby/nostr-wallet-connect/service" + "github.com/getAlby/nostr-wallet-connect/utils" +) + +type api struct { + logger *logrus.Logger + svc service.Service + lspSvc lsp.LSPService + backupSvc backup.BackupService + db *gorm.DB + dbSvc db.DBService +} + +func NewAPI(svc service.Service, logger *logrus.Logger, gormDb *gorm.DB) *api { + + return &api{ + svc: svc, + logger: logger, + db: gormDb, + dbSvc: db.NewDBService(gormDb, logger), + lspSvc: lsp.NewLSPService(svc, logger), + backupSvc: backup.NewBackupService(svc, logger), + } +} + +func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppResponse, error) { + expiresAt, err := api.parseExpiresAt(createAppRequest.ExpiresAt) + if err != nil { + return nil, fmt.Errorf("invalid expiresAt: %v", err) + } + + // request methods are a space separated list of known request kinds TODO: it should be a string array in the API + requestMethods := strings.Split(createAppRequest.RequestMethods, " ") + if len(requestMethods) == 0 { + return nil, fmt.Errorf("won't create an app without request methods") + } + + app, pairingSecretKey, err := api.dbSvc.CreateApp(createAppRequest.Name, createAppRequest.Pubkey, createAppRequest.MaxAmount, createAppRequest.BudgetRenewal, expiresAt, requestMethods) + + if err != nil { + return nil, err + } + + relayUrl := api.svc.GetConfig().GetRelayUrl() + + responseBody := &CreateAppResponse{} + responseBody.Name = createAppRequest.Name + responseBody.Pubkey = app.NostrPubkey + responseBody.PairingSecret = pairingSecretKey + + if createAppRequest.ReturnTo != "" { + returnToUrl, err := url.Parse(createAppRequest.ReturnTo) + if err == nil { + query := returnToUrl.Query() + query.Add("relay", relayUrl) + query.Add("pubkey", api.svc.GetConfig().GetNostrPublicKey()) + // if user.LightningAddress != "" { + // query.Add("lud16", user.LightningAddress) + // } + returnToUrl.RawQuery = query.Encode() + responseBody.ReturnTo = returnToUrl.String() + } + } + + var lud16 string + // if user.LightningAddress != "" { + // lud16 = fmt.Sprintf("&lud16=%s", user.LightningAddress) + // } + responseBody.PairingUri = fmt.Sprintf("nostr+walletconnect://%s?relay=%s&secret=%s%s", api.svc.GetConfig().GetNostrPublicKey(), relayUrl, pairingSecretKey, lud16) + return responseBody, nil +} + +func (api *api) UpdateApp(userApp *db.App, updateAppRequest *UpdateAppRequest) error { + maxAmount := updateAppRequest.MaxAmount + budgetRenewal := updateAppRequest.BudgetRenewal + + requestMethods := updateAppRequest.RequestMethods + if requestMethods == "" { + return fmt.Errorf("won't update an app to have no request methods") + } + newRequestMethods := strings.Split(requestMethods, " ") + + expiresAt, err := api.parseExpiresAt(updateAppRequest.ExpiresAt) + if err != nil { + return fmt.Errorf("invalid expiresAt: %v", err) + } + + err = api.db.Transaction(func(tx *gorm.DB) error { + // Update existing permissions with new budget and expiry + err := tx.Model(&db.AppPermission{}).Where("app_id", userApp.ID).Updates(map[string]interface{}{ + "ExpiresAt": expiresAt, + "MaxAmount": maxAmount, + "BudgetRenewal": budgetRenewal, + }).Error + if err != nil { + return err + } + + var existingPermissions []db.AppPermission + if err := tx.Where("app_id = ?", userApp.ID).Find(&existingPermissions).Error; err != nil { + return err + } + + existingMethodMap := make(map[string]bool) + for _, perm := range existingPermissions { + existingMethodMap[perm.RequestMethod] = true + } + + // Add new permissions + for _, method := range newRequestMethods { + if !existingMethodMap[method] { + perm := db.AppPermission{ + App: *userApp, + RequestMethod: method, + ExpiresAt: expiresAt, + MaxAmount: maxAmount, + BudgetRenewal: budgetRenewal, + } + if err := tx.Create(&perm).Error; err != nil { + return err + } + } + delete(existingMethodMap, method) + } + + // Remove old permissions + for method := range existingMethodMap { + if err := tx.Where("app_id = ? AND request_method = ?", userApp.ID, method).Delete(&db.AppPermission{}).Error; err != nil { + return err + } + } + + // commit transaction + return nil + }) + + return err +} + +func (api *api) DeleteApp(userApp *db.App) error { + return api.db.Delete(userApp).Error +} + +func (api *api) GetApp(userApp *db.App) *App { + + var lastEvent db.RequestEvent + lastEventResult := api.db.Where("app_id = ?", userApp.ID).Order("id desc").Limit(1).Find(&lastEvent) + + paySpecificPermission := db.AppPermission{} + appPermissions := []db.AppPermission{} + var expiresAt *time.Time + api.db.Where("app_id = ?", userApp.ID).Find(&appPermissions) + + requestMethods := []string{} + for _, appPerm := range appPermissions { + expiresAt = appPerm.ExpiresAt + if appPerm.RequestMethod == nip47.PAY_INVOICE_METHOD { + //find the pay_invoice-specific permissions + paySpecificPermission = appPerm + } + requestMethods = append(requestMethods, appPerm.RequestMethod) + } + + //renewsIn := "" + budgetUsage := int64(0) + maxAmount := paySpecificPermission.MaxAmount + if maxAmount > 0 { + budgetUsage = api.svc.GetBudgetUsage(&paySpecificPermission) + } + + response := App{ + Name: userApp.Name, + Description: userApp.Description, + CreatedAt: userApp.CreatedAt, + UpdatedAt: userApp.UpdatedAt, + NostrPubkey: userApp.NostrPubkey, + ExpiresAt: expiresAt, + MaxAmount: maxAmount, + RequestMethods: requestMethods, + BudgetUsage: budgetUsage, + BudgetRenewal: paySpecificPermission.BudgetRenewal, + } + + if lastEventResult.RowsAffected > 0 { + response.LastEventAt = &lastEvent.CreatedAt + } + + return &response + +} + +func (api *api) ListApps() ([]App, error) { + // TODO: join dbApps and permissions + dbApps := []db.App{} + api.db.Find(&dbApps) + + permissions := []db.AppPermission{} + api.db.Find(&permissions) + + permissionsMap := make(map[uint][]db.AppPermission) + for _, perm := range permissions { + permissionsMap[perm.AppId] = append(permissionsMap[perm.AppId], perm) + } + + apiApps := []App{} + for _, userApp := range dbApps { + apiApp := App{ + // ID: app.ID, + Name: userApp.Name, + Description: userApp.Description, + CreatedAt: userApp.CreatedAt, + UpdatedAt: userApp.UpdatedAt, + NostrPubkey: userApp.NostrPubkey, + } + + for _, permission := range permissionsMap[userApp.ID] { + apiApp.RequestMethods = append(apiApp.RequestMethods, permission.RequestMethod) + apiApp.ExpiresAt = permission.ExpiresAt + if permission.RequestMethod == nip47.PAY_INVOICE_METHOD { + apiApp.BudgetRenewal = permission.BudgetRenewal + apiApp.MaxAmount = permission.MaxAmount + if apiApp.MaxAmount > 0 { + apiApp.BudgetUsage = api.svc.GetBudgetUsage(&permission) + } + } + } + + var lastEvent db.RequestEvent + lastEventResult := api.db.Where("app_id = ?", userApp.ID).Order("id desc").Limit(1).Find(&lastEvent) + if lastEventResult.RowsAffected > 0 { + apiApp.LastEventAt = &lastEvent.CreatedAt + } + + apiApps = append(apiApps, apiApp) + } + return apiApps, nil +} + +func (api *api) ListChannels(ctx context.Context) ([]lnclient.Channel, error) { + if api.svc.GetLNClient() == nil { + return nil, errors.New("LNClient not started") + } + return api.svc.GetLNClient().ListChannels(ctx) +} + +func (api *api) GetChannelPeerSuggestions(ctx context.Context) ([]alby.ChannelPeerSuggestion, error) { + return api.svc.GetAlbyOAuthSvc().GetChannelPeerSuggestions(ctx) +} + +func (api *api) ResetRouter(key string) error { + if api.svc.GetLNClient() == nil { + return errors.New("LNClient not started") + } + err := api.svc.GetLNClient().ResetRouter(key) + if err != nil { + return err + } + + // Because the above method has to stop the node to reset the router, + // We also need to stop the lnclient and ask the user to start it again + return api.Stop() +} + +func (api *api) ChangeUnlockPassword(changeUnlockPasswordRequest *ChangeUnlockPasswordRequest) error { + if api.svc.GetLNClient() == nil { + return errors.New("LNClient not started") + } + + err := api.svc.GetConfig().ChangeUnlockPassword(changeUnlockPasswordRequest.CurrentUnlockPassword, changeUnlockPasswordRequest.NewUnlockPassword) + + if err != nil { + api.logger.WithError(err).Error("failed to change unlock password") + return err + } + + // Because all the encrypted fields have changed + // we also need to stop the lnclient and ask the user to start it again + return api.Stop() +} + +func (api *api) Stop() error { + api.logger.Info("Running Stop command") + if api.svc.GetLNClient() == nil { + return errors.New("LNClient not started") + } + // stop the lnclient + // The user will be forced to re-enter their unlock password to restart the node + err := api.svc.StopLNClient() + if err != nil { + api.logger.WithError(err).Error("Failed to stop LNClient") + } + return err +} + +func (api *api) GetNodeConnectionInfo(ctx context.Context) (*lnclient.NodeConnectionInfo, error) { + if api.svc.GetLNClient() == nil { + return nil, errors.New("LNClient not started") + } + return api.svc.GetLNClient().GetNodeConnectionInfo(ctx) +} + +func (api *api) GetNodeStatus(ctx context.Context) (*lnclient.NodeStatus, error) { + if api.svc.GetLNClient() == nil { + return nil, errors.New("LNClient not started") + } + return api.svc.GetLNClient().GetNodeStatus(ctx) +} + +func (api *api) ListPeers(ctx context.Context) ([]lnclient.PeerDetails, error) { + if api.svc.GetLNClient() == nil { + return nil, errors.New("LNClient not started") + } + return api.svc.GetLNClient().ListPeers(ctx) +} + +func (api *api) ConnectPeer(ctx context.Context, connectPeerRequest *ConnectPeerRequest) error { + if api.svc.GetLNClient() == nil { + return errors.New("LNClient not started") + } + return api.svc.GetLNClient().ConnectPeer(ctx, connectPeerRequest) +} + +func (api *api) OpenChannel(ctx context.Context, openChannelRequest *OpenChannelRequest) (*OpenChannelResponse, error) { + if api.svc.GetLNClient() == nil { + return nil, errors.New("LNClient not started") + } + return api.svc.GetLNClient().OpenChannel(ctx, openChannelRequest) +} + +func (api *api) CloseChannel(ctx context.Context, peerId, channelId string, force bool) (*CloseChannelResponse, error) { + if api.svc.GetLNClient() == nil { + return nil, errors.New("LNClient not started") + } + api.logger.WithFields(logrus.Fields{ + "peer_id": peerId, + "channel_id": channelId, + "force": force, + }).Info("Closing channel") + return api.svc.GetLNClient().CloseChannel(ctx, &lnclient.CloseChannelRequest{ + NodeId: peerId, + ChannelId: channelId, + Force: force, + }) +} + +func (api *api) GetNewOnchainAddress(ctx context.Context) (*NewOnchainAddressResponse, error) { + if api.svc.GetLNClient() == nil { + return nil, errors.New("LNClient not started") + } + address, err := api.svc.GetLNClient().GetNewOnchainAddress(ctx) + if err != nil { + return nil, err + } + return &NewOnchainAddressResponse{ + Address: address, + }, nil +} + +func (api *api) SignMessage(ctx context.Context, message string) (*SignMessageResponse, error) { + if api.svc.GetLNClient() == nil { + return nil, errors.New("LNClient not started") + } + signature, err := api.svc.GetLNClient().SignMessage(ctx, message) + if err != nil { + return nil, err + } + return &SignMessageResponse{ + Message: message, + Signature: signature, + }, nil +} + +func (api *api) RedeemOnchainFunds(ctx context.Context, toAddress string) (*RedeemOnchainFundsResponse, error) { + if api.svc.GetLNClient() == nil { + return nil, errors.New("LNClient not started") + } + txId, err := api.svc.GetLNClient().RedeemOnchainFunds(ctx, toAddress) + if err != nil { + return nil, err + } + return &RedeemOnchainFundsResponse{ + TxId: txId, + }, nil +} + +func (api *api) GetBalances(ctx context.Context) (*BalancesResponse, error) { + if api.svc.GetLNClient() == nil { + return nil, errors.New("LNClient not started") + } + balances, err := api.svc.GetLNClient().GetBalances(ctx) + if err != nil { + return nil, err + } + return balances, nil +} + +func (api *api) RequestMempoolApi(endpoint string) (interface{}, error) { + url := api.svc.GetConfig().GetEnv().MempoolApi + endpoint + + client := http.Client{ + Timeout: time.Second * 10, + } + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + api.logger.WithError(err).WithFields(logrus.Fields{ + "url": url, + }).Error("Failed to create http request") + return nil, err + } + + res, err := client.Do(req) + if err != nil { + api.logger.WithError(err).WithFields(logrus.Fields{ + "url": url, + }).Error("Failed to send request") + return nil, err + } + + defer res.Body.Close() + + body, readErr := io.ReadAll(res.Body) + if readErr != nil { + api.logger.WithError(err).WithFields(logrus.Fields{ + "url": url, + }).Error("Failed to read response body") + return nil, errors.New("failed to read response body") + } + + var jsonContent interface{} + jsonErr := json.Unmarshal(body, &jsonContent) + if jsonErr != nil { + api.logger.WithError(jsonErr).WithFields(logrus.Fields{ + "url": url, + }).Error("Failed to deserialize json") + return nil, fmt.Errorf("failed to deserialize json %s %s", url, string(body)) + } + return jsonContent, nil +} + +func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) { + info := InfoResponse{} + backendType, _ := api.svc.GetConfig().Get("LNBackendType", "") + unlockPasswordCheck, _ := api.svc.GetConfig().Get("UnlockPasswordCheck", "") + info.SetupCompleted = unlockPasswordCheck != "" + info.Running = api.svc.GetLNClient() != nil + info.BackendType = backendType + info.AlbyAuthUrl = api.svc.GetAlbyOAuthSvc().GetAuthUrl() + info.OAuthRedirect = !api.svc.GetConfig().GetEnv().IsDefaultClientId() + albyUserIdentifier, err := api.svc.GetAlbyOAuthSvc().GetUserIdentifier() + if err != nil { + api.logger.WithError(err).Error("Failed to get alby user identifier") + return nil, err + } + info.AlbyUserIdentifier = albyUserIdentifier + info.AlbyAccountConnected = api.svc.GetAlbyOAuthSvc().IsConnected(ctx) + if api.svc.GetLNClient() != nil { + nodeInfo, err := api.svc.GetLNClient().GetInfo(ctx) + if err != nil { + api.logger.WithError(err).Error("Failed to get nodeInfo") + return nil, err + } + + info.Network = nodeInfo.Network + } + + info.NextBackupReminder, _ = api.svc.GetConfig().Get("NextBackupReminder", "") + + return &info, nil +} + +func (api *api) GetEncryptedMnemonic() *EncryptedMnemonicResponse { + resp := EncryptedMnemonicResponse{} + mnemonic, _ := api.svc.GetConfig().Get("Mnemonic", "") + resp.Mnemonic = mnemonic + return &resp +} + +func (api *api) SetNextBackupReminder(backupReminderRequest *BackupReminderRequest) error { + api.svc.GetConfig().SetUpdate("NextBackupReminder", backupReminderRequest.NextBackupReminder, "") + return nil +} + +func (api *api) Start(startRequest *StartRequest) error { + return api.svc.StartApp(startRequest.UnlockPassword) +} + +func (api *api) Setup(ctx context.Context, setupRequest *SetupRequest) error { + info, err := api.GetInfo(ctx) + if err != nil { + api.logger.WithError(err).Error("Failed to get info") + return err + } + if info.SetupCompleted { + api.logger.Error("Cannot re-setup node") + return errors.New("setup already completed") + } + + api.svc.GetConfig().Setup(setupRequest.UnlockPassword) + + // TODO: move all below code to cfg.Setup() + + // update next backup reminder + api.svc.GetConfig().SetUpdate("NextBackupReminder", setupRequest.NextBackupReminder, "") + // only update non-empty values + if setupRequest.LNBackendType != "" { + api.svc.GetConfig().SetUpdate("LNBackendType", setupRequest.LNBackendType, "") + } + if setupRequest.BreezAPIKey != "" { + api.svc.GetConfig().SetUpdate("BreezAPIKey", setupRequest.BreezAPIKey, setupRequest.UnlockPassword) + } + if setupRequest.Mnemonic != "" { + api.svc.GetConfig().SetUpdate("Mnemonic", setupRequest.Mnemonic, setupRequest.UnlockPassword) + } + if setupRequest.GreenlightInviteCode != "" { + api.svc.GetConfig().SetUpdate("GreenlightInviteCode", setupRequest.GreenlightInviteCode, setupRequest.UnlockPassword) + } + if setupRequest.LNDAddress != "" { + api.svc.GetConfig().SetUpdate("LNDAddress", setupRequest.LNDAddress, setupRequest.UnlockPassword) + } + if setupRequest.LNDCertHex != "" { + api.svc.GetConfig().SetUpdate("LNDCertHex", setupRequest.LNDCertHex, setupRequest.UnlockPassword) + } + if setupRequest.LNDMacaroonHex != "" { + api.svc.GetConfig().SetUpdate("LNDMacaroonHex", setupRequest.LNDMacaroonHex, setupRequest.UnlockPassword) + } + + return nil +} + +func (api *api) SendPaymentProbes(ctx context.Context, sendPaymentProbesRequest *SendPaymentProbesRequest) (*SendPaymentProbesResponse, error) { + if api.svc.GetLNClient() == nil { + return nil, errors.New("LNClient not started") + } + + var errMessage string + err := api.svc.GetLNClient().SendPaymentProbes(ctx, sendPaymentProbesRequest.Invoice) + if err != nil { + errMessage = err.Error() + } + + return &SendPaymentProbesResponse{Error: errMessage}, nil +} + +func (api *api) SendSpontaneousPaymentProbes(ctx context.Context, sendSpontaneousPaymentProbesRequest *SendSpontaneousPaymentProbesRequest) (*SendSpontaneousPaymentProbesResponse, error) { + if api.svc.GetLNClient() == nil { + return nil, errors.New("LNClient not started") + } + + var errMessage string + err := api.svc.GetLNClient().SendSpontaneousPaymentProbes(ctx, sendSpontaneousPaymentProbesRequest.Amount, sendSpontaneousPaymentProbesRequest.NodeId) + if err != nil { + errMessage = err.Error() + } + + return &SendSpontaneousPaymentProbesResponse{Error: errMessage}, nil +} + +func (api *api) GetNetworkGraph(nodeIds []string) (NetworkGraphResponse, error) { + if api.svc.GetLNClient() == nil { + return nil, errors.New("LNClient not started") + } + return api.svc.GetLNClient().GetNetworkGraph(nodeIds) +} + +func (api *api) SyncWallet() error { + if api.svc.GetLNClient() == nil { + return errors.New("LNClient not started") + } + api.svc.GetLNClient().UpdateLastWalletSyncRequest() + return nil +} + +func (api *api) GetLogOutput(ctx context.Context, logType string, getLogRequest *GetLogOutputRequest) (*GetLogOutputResponse, error) { + var err error + var logData []byte + + if logType == LogTypeNode { + if api.svc.GetLNClient() == nil { + return nil, errors.New("LNClient not started") + } + + logData, err = api.svc.GetLNClient().GetLogOutput(ctx, getLogRequest.MaxLen) + if err != nil { + return nil, err + } + } else if logType == LogTypeApp { + logFileName := api.svc.GetLogFilePath() + + logData, err = utils.ReadFileTail(logFileName, getLogRequest.MaxLen) + if err != nil { + return nil, err + } + } else { + return nil, fmt.Errorf("invalid log type: '%s'", logType) + } + + return &GetLogOutputResponse{Log: string(logData)}, nil +} + +func (api *api) parseExpiresAt(expiresAtString string) (*time.Time, error) { + var expiresAt *time.Time + if expiresAtString != "" { + var err error + expiresAtValue, err := time.Parse(time.RFC3339, expiresAtString) + if err != nil { + api.logger.WithField("expiresAt", expiresAtString).Error("Invalid expiresAt") + return nil, fmt.Errorf("invalid expiresAt: %v", err) + } + expiresAtValue = time.Date(expiresAtValue.Year(), expiresAtValue.Month(), expiresAtValue.Day(), 23, 59, 59, 0, expiresAtValue.Location()) + expiresAt = &expiresAtValue + } + return expiresAt, nil +} + +func (api *api) GetLSPService() lsp.LSPService { + return api.lspSvc +} +func (api *api) GetBackupService() backup.BackupService { + return api.backupSvc +} diff --git a/models/api/api.go b/api/models.go similarity index 65% rename from models/api/api.go rename to api/models.go index b2ab4f4a..3bd43afe 100644 --- a/models/api/api.go +++ b/api/models.go @@ -1,14 +1,50 @@ -// TODO: move to api/models.go package api import ( + "context" "time" - "github.com/getAlby/nostr-wallet-connect/models/lnclient" + "github.com/getAlby/nostr-wallet-connect/alby" + "github.com/getAlby/nostr-wallet-connect/backup" + "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/lsp" ) type API interface { CreateApp(createAppRequest *CreateAppRequest) (*CreateAppResponse, error) + UpdateApp(userApp *db.App, updateAppRequest *UpdateAppRequest) error + DeleteApp(userApp *db.App) error + GetApp(userApp *db.App) *App + ListApps() ([]App, error) + ListChannels(ctx context.Context) ([]lnclient.Channel, error) + GetChannelPeerSuggestions(ctx context.Context) ([]alby.ChannelPeerSuggestion, error) + ResetRouter(key string) error + ChangeUnlockPassword(changeUnlockPasswordRequest *ChangeUnlockPasswordRequest) error + Stop() error + GetNodeConnectionInfo(ctx context.Context) (*lnclient.NodeConnectionInfo, error) + GetNodeStatus(ctx context.Context) (*lnclient.NodeStatus, error) + ListPeers(ctx context.Context) ([]lnclient.PeerDetails, error) + ConnectPeer(ctx context.Context, connectPeerRequest *ConnectPeerRequest) error + OpenChannel(ctx context.Context, openChannelRequest *OpenChannelRequest) (*OpenChannelResponse, error) + CloseChannel(ctx context.Context, peerId, channelId string, force bool) (*CloseChannelResponse, error) + GetNewOnchainAddress(ctx context.Context) (*NewOnchainAddressResponse, error) + SignMessage(ctx context.Context, message string) (*SignMessageResponse, error) + RedeemOnchainFunds(ctx context.Context, toAddress string) (*RedeemOnchainFundsResponse, error) + GetBalances(ctx context.Context) (*BalancesResponse, error) + RequestMempoolApi(endpoint string) (interface{}, error) + GetInfo(ctx context.Context) (*InfoResponse, error) + GetEncryptedMnemonic() *EncryptedMnemonicResponse + SetNextBackupReminder(backupReminderRequest *BackupReminderRequest) error + Start(startRequest *StartRequest) error + Setup(ctx context.Context, setupRequest *SetupRequest) error + SendPaymentProbes(ctx context.Context, sendPaymentProbesRequest *SendPaymentProbesRequest) (*SendPaymentProbesResponse, error) + SendSpontaneousPaymentProbes(ctx context.Context, sendSpontaneousPaymentProbesRequest *SendSpontaneousPaymentProbesRequest) (*SendSpontaneousPaymentProbesResponse, error) + GetNetworkGraph(nodeIds []string) (NetworkGraphResponse, error) + SyncWallet() error + GetLogOutput(ctx context.Context, logType string, getLogRequest *GetLogOutputRequest) (*GetLogOutputResponse, error) + GetLSPService() lsp.LSPService + GetBackupService() backup.BackupService } type App struct { @@ -96,11 +132,10 @@ type InfoResponse struct { BackendType string `json:"backendType"` SetupCompleted bool `json:"setupCompleted"` OAuthRedirect bool `json:"oauthRedirect"` - OnboardingCompleted bool `json:"onboardingCompleted"` // TODO: rename - HasChannel? Running bool `json:"running"` Unlocked bool `json:"unlocked"` AlbyAuthUrl string `json:"albyAuthUrl"` - ShowBackupReminder bool `json:"showBackupReminder"` + NextBackupReminder string `json:"nextBackupReminder"` AlbyUserIdentifier string `json:"albyUserIdentifier"` AlbyAccountConnected bool `json:"albyAccountConnected"` Network string `json:"network"` @@ -120,16 +155,6 @@ type OpenChannelRequest = lnclient.OpenChannelRequest type OpenChannelResponse = lnclient.OpenChannelResponse type CloseChannelResponse = lnclient.CloseChannelResponse -type NewInstantChannelInvoiceRequest struct { - Amount uint64 `json:"amount"` - LSP string `json:"lsp"` -} - -type NewInstantChannelInvoiceResponse struct { - Invoice string `json:"invoice"` - Fee uint64 `json:"fee"` -} - type RedeemOnchainFundsRequest struct { ToAddress string `json:"toAddress"` } @@ -185,15 +210,6 @@ type SignMessageResponse struct { Signature string `json:"signature"` } -// TODO: move to different file -type AlbyBalanceResponse struct { - Sats int64 `json:"sats"` -} - -type AlbyPayRequest struct { - Invoice string `json:"invoice"` -} - type ResetRouterRequest struct { Key string `json:"key"` } diff --git a/backup/backup_service.go b/backup/backup_service.go new file mode 100644 index 00000000..9bb06ed4 --- /dev/null +++ b/backup/backup_service.go @@ -0,0 +1,295 @@ +package backup + +import ( + "errors" + "fmt" + "io" + "strings" + "time" + + "archive/zip" + "os" + "path/filepath" + "slices" + + "github.com/getAlby/nostr-wallet-connect/service" + "github.com/sirupsen/logrus" + + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + + "golang.org/x/crypto/pbkdf2" +) + +type backupService struct { + svc service.Service + logger *logrus.Logger +} + +func NewBackupService(svc service.Service, logger *logrus.Logger) *backupService { + return &backupService{ + svc: svc, + logger: logger, + } +} + +func (bs *backupService) CreateBackup(unlockPassword string, w io.Writer) error { + var err error + + if !bs.svc.GetConfig().CheckUnlockPassword(unlockPassword) { + return errors.New("invalid unlock password") + } + + workDir, err := filepath.Abs(bs.svc.GetConfig().GetEnv().Workdir) + if err != nil { + return fmt.Errorf("failed to get absolute workdir: %w", err) + } + + lnStorageDir := "" + + if bs.svc.GetLNClient() == nil { + return fmt.Errorf("node not running") + } + lnStorageDir, err = bs.svc.GetLNClient().GetStorageDir() + if err != nil { + return fmt.Errorf("failed to get storage dir: %w", err) + } + bs.logger.WithField("path", lnStorageDir).Info("Found node storage dir") + + // Reset the routing data to decrease the LDK DB size + err = bs.svc.GetLNClient().ResetRouter("ALL") + if err != nil { + bs.logger.WithError(err).Error("Failed to reset router") + return fmt.Errorf("failed to reset router: %w", err) + } + // Stop the app to ensure no new requests are processed. + bs.svc.StopApp() + + // Closing the database leaves the service in an inconsistent state, + // but that should not be a problem since the app is not expected + // to be used after its data is exported. + err = bs.svc.StopDb() + if err != nil { + bs.logger.WithError(err).Error("Failed to stop database") + return fmt.Errorf("failed to close database: %w", err) + } + + var filesToArchive []string + + if lnStorageDir != "" { + lnFiles, err := filepath.Glob(filepath.Join(workDir, lnStorageDir, "*")) + if err != nil { + return fmt.Errorf("failed to list files in the LNClient storage directory: %w", err) + } + bs.logger.WithField("lnFiles", lnFiles).Info("Listed node storage dir") + + // Avoid backing up log files. + slices.DeleteFunc(lnFiles, func(s string) bool { + return filepath.Ext(s) == ".log" + }) + + filesToArchive = append(filesToArchive, lnFiles...) + } + + cw, err := encryptingWriter(w, unlockPassword) + if err != nil { + return fmt.Errorf("failed to create encrypted writer: %w", err) + } + + zw := zip.NewWriter(cw) + defer zw.Close() + + addFileToZip := func(fsPath, zipPath string) error { + inF, err := os.Open(fsPath) + if err != nil { + return fmt.Errorf("failed to open source file for reading: %w", err) + } + defer inF.Close() + + outW, err := zw.Create(zipPath) + if err != nil { + return fmt.Errorf("failed to create zip entry: %w", err) + } + + _, err = io.Copy(outW, inF) + return err + } + + // Locate the main database file. + dbFilePath := bs.svc.GetConfig().GetEnv().DatabaseUri + // Add the database file to the archive. + bs.logger.WithField("nwc.db", dbFilePath).Info("adding nwc db to zip") + err = addFileToZip(dbFilePath, "nwc.db") + if err != nil { + bs.logger.WithError(err).Error("Failed to zip nwc db") + return fmt.Errorf("failed to write nwc db file to zip: %w", err) + } + + for _, fileToArchive := range filesToArchive { + bs.logger.WithField("fileToArchive", fileToArchive).Info("adding file to zip") + relPath, err := filepath.Rel(workDir, fileToArchive) + if err != nil { + bs.logger.WithError(err).Error("Failed to get relative path of input file") + return fmt.Errorf("failed to get relative path of input file: %w", err) + } + + // Ensure forward slashes for zip format compatibility. + err = addFileToZip(fileToArchive, filepath.ToSlash(relPath)) + if err != nil { + bs.logger.WithError(err).Error("Failed to write file to zip") + return fmt.Errorf("failed to write input file to zip: %w", err) + } + } + + return nil +} + +func (bs *backupService) RestoreBackup(unlockPassword string, r io.Reader) error { + workDir, err := filepath.Abs(bs.svc.GetConfig().GetEnv().Workdir) + if err != nil { + return fmt.Errorf("failed to get absolute workdir: %w", err) + } + + if strings.HasPrefix(bs.svc.GetConfig().GetEnv().DatabaseUri, "file:") { + return errors.New("cannot restore backup when database path is a file URI") + } + + cr, err := decryptingReader(r, unlockPassword) + if err != nil { + return fmt.Errorf("failed to create decrypted reader: %w", err) + } + + tmpF, err := os.CreateTemp("", "nwc-*.bkp") + if err != nil { + return fmt.Errorf("failed to create temporary output file: %w", err) + } + tmpName := tmpF.Name() + defer os.Remove(tmpName) + defer tmpF.Close() + + zipSize, err := io.Copy(tmpF, cr) + if err != nil { + return fmt.Errorf("failed to decrypt backup data into temporary file: %w", err) + } + + if err = tmpF.Sync(); err != nil { + return fmt.Errorf("failed to flush temporary file: %w", err) + } + + if _, err = tmpF.Seek(0, 0); err != nil { + return fmt.Errorf("failed to seek to beginning of temporary file: %w", err) + } + + zr, err := zip.NewReader(tmpF, zipSize) + if err != nil { + return fmt.Errorf("failed to create zip reader: %w", err) + } + + extractZipEntry := func(zipFile *zip.File) error { + fsFilePath := filepath.Join(workDir, "restore", filepath.FromSlash(zipFile.Name)) + + if err = os.MkdirAll(filepath.Dir(fsFilePath), 0700); err != nil { + return fmt.Errorf("failed to create directory for zip entry: %w", err) + } + + inF, err := zipFile.Open() + if err != nil { + return fmt.Errorf("failed to open zip entry for reading: %w", err) + } + defer inF.Close() + + outF, err := os.OpenFile(fsFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + return fmt.Errorf("failed to create destination file: %w", err) + } + defer outF.Close() + + if _, err = io.Copy(outF, inF); err != nil { + return fmt.Errorf("failed to write zip entry to destination file: %w", err) + } + + return nil + } + + bs.logger.WithField("count", len(zr.File)).Info("Extracting files") + for _, f := range zr.File { + bs.logger.WithField("file", f.Name).Info("Extracting file") + if err = extractZipEntry(f); err != nil { + return fmt.Errorf("failed to extract zip entry: %w", err) + } + } + bs.logger.WithField("count", len(zr.File)).Info("Extracted files") + + go func() { + bs.logger.Info("Backup restored. Shutting down Alby Hub...") + // schedule node shutdown after a few seconds to ensure frontend updates + time.Sleep(5 * time.Second) + os.Exit(0) + }() + + return nil +} + +func encryptingWriter(w io.Writer, password string) (io.Writer, error) { + salt := make([]byte, 8) + if _, err := rand.Read(salt); err != nil { + return nil, fmt.Errorf("failed to generate salt: %w", err) + } + + encKey := pbkdf2.Key([]byte(password), salt, 4096, 32, sha256.New) + block, err := aes.NewCipher(encKey) + if err != nil { + return nil, fmt.Errorf("failed to create AES cipher: %w", err) + } + + iv := make([]byte, aes.BlockSize) + if _, err = rand.Read(iv); err != nil { + return nil, fmt.Errorf("failed to generate IV: %w", err) + } + + _, err = w.Write(salt) + if err != nil { + return nil, fmt.Errorf("failed to write salt: %w", err) + } + + _, err = w.Write(iv) + if err != nil { + return nil, fmt.Errorf("failed to write IV: %w", err) + } + + stream := cipher.NewOFB(block, iv) + cw := &cipher.StreamWriter{ + S: stream, + W: w, + } + + return cw, nil +} + +func decryptingReader(r io.Reader, password string) (io.Reader, error) { + salt := make([]byte, 8) + if _, err := io.ReadFull(r, salt); err != nil { + return nil, fmt.Errorf("failed to read salt: %w", err) + } + + iv := make([]byte, aes.BlockSize) + if _, err := io.ReadFull(r, iv); err != nil { + return nil, fmt.Errorf("failed to read IV: %w", err) + } + + encKey := pbkdf2.Key([]byte(password), salt, 4096, 32, sha256.New) + block, err := aes.NewCipher(encKey) + if err != nil { + return nil, fmt.Errorf("failed to create AES cipher: %w", err) + } + + stream := cipher.NewOFB(block, iv) + cr := &cipher.StreamReader{ + S: stream, + R: r, + } + + return cr, nil +} diff --git a/backup/models.go b/backup/models.go new file mode 100644 index 00000000..c2ad02d2 --- /dev/null +++ b/backup/models.go @@ -0,0 +1,10 @@ +package backup + +import ( + "io" +) + +type BackupService interface { + CreateBackup(unlockPassword string, w io.Writer) error + RestoreBackup(unlockPassword string, r io.Reader) error +} diff --git a/aesgcm.go b/config/aesgcm.go similarity index 99% rename from aesgcm.go rename to config/aesgcm.go index 7e562b19..250db437 100644 --- a/aesgcm.go +++ b/config/aesgcm.go @@ -1,4 +1,4 @@ -package main +package config import ( "crypto/aes" diff --git a/config.go b/config/config.go similarity index 73% rename from config.go rename to config/config.go index 44482b52..4cc10e77 100644 --- a/config.go +++ b/config/config.go @@ -1,4 +1,4 @@ -package main +package config import ( "crypto/rand" @@ -7,16 +7,16 @@ import ( "fmt" "os" + "github.com/nbd-wtf/go-nostr" "github.com/sirupsen/logrus" "gorm.io/gorm" "gorm.io/gorm/clause" - "github.com/getAlby/nostr-wallet-connect/models/config" - dbModels "github.com/getAlby/nostr-wallet-connect/models/db" + dbModels "github.com/getAlby/nostr-wallet-connect/db" ) -type Config struct { - Env *config.AppConfig +type config struct { + Env *AppConfig CookieSecret string NostrSecretKey string NostrPublicKey string @@ -28,7 +28,13 @@ const ( unlockPasswordCheck = "THIS STRING SHOULD MATCH IF PASSWORD IS CORRECT" ) -func (cfg *Config) Init(db *gorm.DB, env *config.AppConfig, logger *logrus.Logger) { +func NewConfig(db *gorm.DB, env *AppConfig, logger *logrus.Logger) *config { + cfg := &config{} + cfg.init(db, env, logger) + return cfg +} + +func (cfg *config) init(db *gorm.DB, env *AppConfig, logger *logrus.Logger) { cfg.db = db cfg.Env = env cfg.logger = logger @@ -70,15 +76,28 @@ func (cfg *Config) Init(db *gorm.DB, env *config.AppConfig, logger *logrus.Logge } } -func (cfg *Config) GetNostrPublicKey() string { +func (cfg *config) GetNostrPublicKey() string { return cfg.NostrPublicKey } -func (cfg *Config) Get(key string, encryptionKey string) (string, error) { +func (cfg *config) GetNostrSecretKey() string { + return cfg.NostrSecretKey +} + +func (cfg *config) GetCookieSecret() string { + return cfg.CookieSecret +} + +func (cfg *config) GetRelayUrl() string { + relayUrl, _ := cfg.Get("Relay", "") + return relayUrl +} + +func (cfg *config) Get(key string, encryptionKey string) (string, error) { return cfg.get(key, encryptionKey, cfg.db) } -func (cfg *Config) get(key string, encryptionKey string, db *gorm.DB) (string, error) { +func (cfg *config) get(key string, encryptionKey string, db *gorm.DB) (string, error) { var userConfig dbModels.UserConfig err := db.Where(&dbModels.UserConfig{Key: key}).Limit(1).Find(&userConfig).Error if err != nil { @@ -96,7 +115,7 @@ func (cfg *Config) get(key string, encryptionKey string, db *gorm.DB) (string, e return value, nil } -func (cfg *Config) set(key string, value string, clauses clause.OnConflict, encryptionKey string, db *gorm.DB) error { +func (cfg *config) set(key string, value string, clauses clause.OnConflict, encryptionKey string, db *gorm.DB) error { if encryptionKey != "" { encrypted, err := AesGcmEncrypt(value, encryptionKey) if err != nil { @@ -113,7 +132,7 @@ func (cfg *Config) set(key string, value string, clauses clause.OnConflict, encr return nil } -func (cfg *Config) SetIgnore(key string, value string, encryptionKey string) { +func (cfg *config) SetIgnore(key string, value string, encryptionKey string) { clauses := clause.OnConflict{ Columns: []clause.Column{{Name: "key"}}, DoNothing: true, @@ -124,7 +143,7 @@ func (cfg *Config) SetIgnore(key string, value string, encryptionKey string) { } } -func (cfg *Config) SetUpdate(key string, value string, encryptionKey string) { +func (cfg *config) SetUpdate(key string, value string, encryptionKey string) { clauses := clause.OnConflict{ Columns: []clause.Column{{Name: "key"}}, DoUpdates: clause.AssignmentColumns([]string{"value"}), @@ -135,7 +154,7 @@ func (cfg *Config) SetUpdate(key string, value string, encryptionKey string) { } } -func (cfg *Config) ChangeUnlockPassword(currentUnlockPassword string, newUnlockPassword string) error { +func (cfg *config) ChangeUnlockPassword(currentUnlockPassword string, newUnlockPassword string) error { if !cfg.CheckUnlockPassword(currentUnlockPassword) { return errors.New("incorrect password") } @@ -179,16 +198,37 @@ func (cfg *Config) ChangeUnlockPassword(currentUnlockPassword string, newUnlockP return nil } -func (cfg *Config) CheckUnlockPassword(encryptionKey string) bool { +func (cfg *config) CheckUnlockPassword(encryptionKey string) bool { decryptedValue, err := cfg.Get("UnlockPasswordCheck", encryptionKey) return err == nil && (decryptedValue == "" || decryptedValue == unlockPasswordCheck) } -func (cfg *Config) SavePasswordCheck(encryptionKey string) { +func (cfg *config) Setup(encryptionKey string) { cfg.SetUpdate("UnlockPasswordCheck", unlockPasswordCheck, encryptionKey) } +func (cfg *config) Start(encryptionKey string) error { + nostrSecretKey, _ := cfg.Get("NostrSecretKey", encryptionKey) + + if nostrSecretKey == "" { + nostrSecretKey = nostr.GeneratePrivateKey() + cfg.SetUpdate("NostrSecretKey", nostrSecretKey, encryptionKey) + } + nostrPublicKey, err := nostr.GetPublicKey(nostrSecretKey) + if err != nil { + cfg.logger.WithError(err).Error("Error converting nostr privkey to pubkey") + return err + } + cfg.NostrSecretKey = nostrSecretKey + cfg.NostrPublicKey = nostrPublicKey + return nil +} + +func (cfg *config) GetEnv() *AppConfig { + return cfg.Env +} + func randomHex(n int) (string, error) { bytes := make([]byte, n) if _, err := rand.Read(bytes); err != nil { diff --git a/models/config/config.go b/config/models.go similarity index 89% rename from models/config/config.go rename to config/models.go index ca626960..550f2bc9 100644 --- a/models/config/config.go +++ b/config/models.go @@ -1,4 +1,3 @@ -// TODO: move to config/models.go package config const ( @@ -45,4 +44,12 @@ type Config interface { SetIgnore(key string, value string, encryptionKey string) SetUpdate(key string, value string, encryptionKey string) GetNostrPublicKey() string + GetNostrSecretKey() string + GetCookieSecret() string + GetRelayUrl() string + GetEnv() *AppConfig + CheckUnlockPassword(password string) bool + ChangeUnlockPassword(currentUnlockPassword string, newUnlockPassword string) error + Setup(encryptionKey string) + Start(encryptionKey string) error } diff --git a/db/db_service.go b/db/db_service.go new file mode 100644 index 00000000..835ccb12 --- /dev/null +++ b/db/db_service.go @@ -0,0 +1,79 @@ +package db + +import ( + "encoding/hex" + "fmt" + "strings" + "time" + + "github.com/getAlby/nostr-wallet-connect/nip47" + "github.com/nbd-wtf/go-nostr" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type dbService struct { + db *gorm.DB + logger *logrus.Logger +} + +func NewDBService(db *gorm.DB, logger *logrus.Logger) *dbService { + return &dbService{ + db: db, + logger: logger, + } +} + +func (dbSvc *dbService) CreateApp(name string, pubkey string, maxAmount int, budgetRenewal string, expiresAt *time.Time, requestMethods []string) (*App, string, error) { + var pairingPublicKey string + var pairingSecretKey string + if pubkey == "" { + pairingSecretKey = nostr.GeneratePrivateKey() + pairingPublicKey, _ = nostr.GetPublicKey(pairingSecretKey) + } else { + pairingPublicKey = pubkey + //validate public key + decoded, err := hex.DecodeString(pairingPublicKey) + if err != nil || len(decoded) != 32 { + dbSvc.logger.WithField("pairingPublicKey", pairingPublicKey).Error("Invalid public key format") + return nil, "", fmt.Errorf("invalid public key format: %s", pairingPublicKey) + } + } + + app := App{Name: name, NostrPubkey: pairingPublicKey} + + err := dbSvc.db.Transaction(func(tx *gorm.DB) error { + err := tx.Save(&app).Error + if err != nil { + return err + } + + for _, m := range requestMethods { + //if we don't know this method, we return an error + if !strings.Contains(nip47.CAPABILITIES, m) { + return fmt.Errorf("did not recognize request method: %s", m) + } + appPermission := AppPermission{ + App: app, + RequestMethod: m, + ExpiresAt: expiresAt, + //these fields are only relevant for pay_invoice + MaxAmount: maxAmount, + BudgetRenewal: budgetRenewal, + } + err = tx.Create(&appPermission).Error + if err != nil { + return err + } + } + // commit transaction + return nil + }) + + if err != nil { + dbSvc.logger.WithError(err).Error("Failed to save app") + return nil, "", err + } + + return &app, pairingSecretKey, nil +} diff --git a/db/models.go b/db/models.go new file mode 100644 index 00000000..14dbfed4 --- /dev/null +++ b/db/models.go @@ -0,0 +1,83 @@ +package db + +import "time" + +type UserConfig struct { + ID uint + Key string + Value string + Encrypted bool + CreatedAt time.Time + UpdatedAt time.Time +} + +type App struct { + ID uint + Name string `validate:"required"` + Description string + NostrPubkey string `validate:"required"` + CreatedAt time.Time + UpdatedAt time.Time +} + +type AppPermission struct { + ID uint + AppId uint `validate:"required"` + App App + RequestMethod string `validate:"required"` + MaxAmount int + BudgetRenewal string + ExpiresAt *time.Time + CreatedAt time.Time + UpdatedAt time.Time +} + +type RequestEvent struct { + ID uint + AppId *uint + App App + NostrId string `validate:"required"` + Content string + State string + CreatedAt time.Time + UpdatedAt time.Time +} + +type ResponseEvent struct { + ID uint + NostrId string `validate:"required"` + RequestId uint `validate:"required"` + Content string + State string + RepliedAt time.Time + CreatedAt time.Time + UpdatedAt time.Time +} + +type Payment struct { + ID uint + AppId uint `validate:"required"` + App App + RequestEventId uint `validate:"required"` + RequestEvent RequestEvent + Amount uint // in sats + PaymentRequest string + Preimage *string + CreatedAt time.Time + UpdatedAt time.Time +} + +type DBService interface { + CreateApp(name string, pubkey string, maxAmount int, budgetRenewal string, expiresAt *time.Time, requestMethods []string) (*App, string, error) +} + +const ( + REQUEST_EVENT_STATE_HANDLER_EXECUTING = "executing" + REQUEST_EVENT_STATE_HANDLER_EXECUTED = "executed" + REQUEST_EVENT_STATE_HANDLER_ERROR = "error" +) +const ( + RESPONSE_EVENT_STATE_PUBLISH_CONFIRMED = "confirmed" + RESPONSE_EVENT_STATE_PUBLISH_FAILED = "failed" + RESPONSE_EVENT_STATE_PUBLISH_UNCONFIRMED = "unconfirmed" +) diff --git a/events/events.go b/events/events.go index 0a6ac669..dab2ee5c 100644 --- a/events/events.go +++ b/events/events.go @@ -8,21 +8,6 @@ import ( "github.com/sirupsen/logrus" ) -type EventSubscriber interface { - ConsumeEvent(ctx context.Context, event *Event, globalProperties map[string]interface{}) error -} - -type Event struct { - Event string `json:"event"` - Properties interface{} `json:"properties,omitempty"` -} - -type PaymentReceivedEventProperties struct { - PaymentHash string `json:"payment_hash"` - Amount uint64 `json:"amount"` - NodeType string `json:"node_type"` -} - type eventPublisher struct { logger *logrus.Logger listeners []EventSubscriber @@ -30,13 +15,6 @@ type eventPublisher struct { globalProperties map[string]interface{} } -type EventPublisher interface { - RegisterSubscriber(eventListener EventSubscriber) - RemoveSubscriber(eventListener EventSubscriber) - Publish(event *Event) - SetGlobalProperty(key string, value interface{}) -} - func NewEventPublisher(logger *logrus.Logger) *eventPublisher { eventPublisher := &eventPublisher{ logger: logger, diff --git a/events/models.go b/events/models.go new file mode 100644 index 00000000..2034ff92 --- /dev/null +++ b/events/models.go @@ -0,0 +1,25 @@ +package events + +import "context" + +type EventSubscriber interface { + ConsumeEvent(ctx context.Context, event *Event, globalProperties map[string]interface{}) error +} + +type EventPublisher interface { + RegisterSubscriber(eventListener EventSubscriber) + RemoveSubscriber(eventListener EventSubscriber) + Publish(event *Event) + SetGlobalProperty(key string, value interface{}) +} + +type Event struct { + Event string `json:"event"` + Properties interface{} `json:"properties,omitempty"` +} + +type PaymentReceivedEventProperties struct { + PaymentHash string `json:"payment_hash"` + Amount uint64 `json:"amount"` + NodeType string `json:"node_type"` +} diff --git a/frontend/src/components/SidebarHint.tsx b/frontend/src/components/SidebarHint.tsx index cf304fbd..4ed82f57 100644 --- a/frontend/src/components/SidebarHint.tsx +++ b/frontend/src/components/SidebarHint.tsx @@ -14,6 +14,7 @@ import { useAlbyMe } from "src/hooks/useAlbyMe"; import { useChannels } from "src/hooks/useChannels"; import { useInfo } from "src/hooks/useInfo"; import { useNodeConnectionInfo } from "src/hooks/useNodeConnectionInfo"; +import { backendTypeHasMnemonic } from "src/lib/utils"; import useChannelOrderStore from "src/state/ChannelOrderStore"; function SidebarHint() { @@ -34,7 +35,7 @@ function SidebarHint() { } // User has a channel order - if (order) { + if (order && order.status !== "pay") { return ( node.public_key === nodeId)?.alias || - "Unknown Node" + `Are you sure you want to close the channel with ${ + nodes.find((node) => node.public_key === nodeId)?.alias || + "Unknown Node" }?\n\nNode ID: ${nodeId}\n\nChannel ID: ${channelId}` ) ) { @@ -139,7 +140,8 @@ export default function Channels() { console.log(`🎬 Closing channel with ${nodeId}`); const closeChannelResponse = await request( - `/api/peers/${nodeId}/channels/${channelId}?force=${closeType === "force close" + `/api/peers/${nodeId}/channels/${channelId}?force=${ + closeType === "force close" }`, { method: "DELETE", @@ -203,36 +205,6 @@ export default function Channels() { } } - async function stopNode() { - try { - if (!csrf) { - throw new Error("csrf not loaded"); - } - - if ( - !confirm( - "After restarting, you'll need to re-enter your unlock password." - ) - ) { - console.error("User cancelled reset"); - return; - } - - await request("/api/stop", { - method: "POST", - headers: { - "X-CSRF-Token": csrf, - "Content-Type": "application/json", - }, - }); - await reloadInfo(); - alert(`🎉 Node stopped`); - } catch (error) { - console.error(error); - alert("Something went wrong: " + error); - } - } - return ( <> Clear Routing Data - - Restart - )} diff --git a/frontend/src/screens/channels/CurrentChannelOrder.tsx b/frontend/src/screens/channels/CurrentChannelOrder.tsx index e56e2738..16555946 100644 --- a/frontend/src/screens/channels/CurrentChannelOrder.tsx +++ b/frontend/src/screens/channels/CurrentChannelOrder.tsx @@ -530,7 +530,8 @@ function PayLightningChannelOrder({ order }: { order: NewChannelOrder }) { channels && prevChannels ? channels.find( (newChannel) => - !prevChannels.some((current) => current.id === newChannel.id) + !prevChannels.some((current) => current.id === newChannel.id) && + newChannel.fundingTxId ) : undefined; diff --git a/frontend/src/screens/setup/SetupPassword.tsx b/frontend/src/screens/setup/SetupPassword.tsx index 79699c39..8b8e15a1 100644 --- a/frontend/src/screens/setup/SetupPassword.tsx +++ b/frontend/src/screens/setup/SetupPassword.tsx @@ -11,6 +11,7 @@ import { Input } from "src/components/ui/input"; import { Label } from "src/components/ui/label"; import { useToast } from "src/components/ui/use-toast"; import { useInfo } from "src/hooks/useInfo"; +import { backendTypeHasMnemonic } from "src/lib/utils"; export function SetupPassword() { const { toast } = useToast(); @@ -23,6 +24,9 @@ export function SetupPassword() { function onSubmit(e: React.FormEvent) { e.preventDefault(); + if (!info) { + return; + } if (store.unlockPassword !== confirmPassword) { toast({ title: "Passwords don't match", @@ -31,8 +35,7 @@ export function SetupPassword() { return; } - // Pre-configured nodes that do not need a mnemonic - if (info?.backendType === "LND" || info?.backendType === "PHOENIX") { + if (!backendTypeHasMnemonic(info.backendType)) { // NOTE: LND flow does not setup a mnemonic navigate(`/setup/finish`); return; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index a386f24c..bfed2009 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -133,12 +133,11 @@ export interface InfoResponse { backendType: BackendType; setupCompleted: boolean; oauthRedirect: boolean; - onboardingCompleted: boolean; albyAccountConnected: boolean; running: boolean; unlocked: boolean; albyAuthUrl: string; - showBackupReminder: boolean; + nextBackupReminder: string; albyUserIdentifier: string; network?: Network; } diff --git a/handle_balance_request.go b/handle_balance_request.go index f2fccc5e..dd85d33f 100644 --- a/handle_balance_request.go +++ b/handle_balance_request.go @@ -3,6 +3,7 @@ package main import ( "context" + "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/nbd-wtf/go-nostr" "github.com/sirupsen/logrus" @@ -12,7 +13,7 @@ const ( MSAT_PER_SAT = 1000 ) -func (svc *Service) HandleGetBalanceEvent(ctx context.Context, nip47Request *Nip47Request, requestEvent *RequestEvent, app *App, publishResponse func(*Nip47Response, nostr.Tags)) { +func (svc *Service) HandleGetBalanceEvent(ctx context.Context, nip47Request *nip47.Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*nip47.Response, nostr.Tags)) { resp := svc.checkPermission(nip47Request, requestEvent.NostrId, app, 0) if resp != nil { @@ -20,20 +21,20 @@ func (svc *Service) HandleGetBalanceEvent(ctx context.Context, nip47Request *Nip return } - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, }).Info("Fetching balance") balance, err := svc.lnClient.GetBalance(ctx) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, }).Infof("Failed to fetch balance: %v", err) - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, - Error: &Nip47Error{ + Error: &nip47.Error{ Code: nip47.ERROR_INTERNAL, Message: err.Error(), }, @@ -41,11 +42,11 @@ func (svc *Service) HandleGetBalanceEvent(ctx context.Context, nip47Request *Nip return } - responsePayload := &Nip47BalanceResponse{ + responsePayload := &nip47.BalanceResponse{ Balance: balance, } - appPermission := AppPermission{} + appPermission := db.AppPermission{} svc.db.Where("app_id = ? AND request_method = ?", app.ID, nip47.PAY_INVOICE_METHOD).First(&appPermission) maxAmount := appPermission.MaxAmount @@ -54,7 +55,7 @@ func (svc *Service) HandleGetBalanceEvent(ctx context.Context, nip47Request *Nip responsePayload.BudgetRenewal = appPermission.BudgetRenewal } - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, Result: responsePayload, }, nostr.Tags{}) diff --git a/handle_info_request.go b/handle_info_request.go index 5c77674e..f2f610ed 100644 --- a/handle_info_request.go +++ b/handle_info_request.go @@ -3,12 +3,13 @@ package main import ( "context" + "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/nbd-wtf/go-nostr" "github.com/sirupsen/logrus" ) -func (svc *Service) HandleGetInfoEvent(ctx context.Context, nip47Request *Nip47Request, requestEvent *RequestEvent, app *App, publishResponse func(*Nip47Response, nostr.Tags)) { +func (svc *Service) HandleGetInfoEvent(ctx context.Context, nip47Request *nip47.Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*nip47.Response, nostr.Tags)) { resp := svc.checkPermission(nip47Request, requestEvent.NostrId, app, 0) if resp != nil { @@ -16,21 +17,21 @@ func (svc *Service) HandleGetInfoEvent(ctx context.Context, nip47Request *Nip47R return } - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, }).Info("Fetching node info") info, err := svc.lnClient.GetInfo(ctx) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, }).Infof("Failed to fetch node info: %v", err) - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, - Error: &Nip47Error{ + Error: &nip47.Error{ Code: nip47.ERROR_INTERNAL, Message: err.Error(), }, @@ -44,7 +45,7 @@ func (svc *Service) HandleGetInfoEvent(ctx context.Context, nip47Request *Nip47R network = "mainnet" } - responsePayload := &Nip47GetInfoResponse{ + responsePayload := &nip47.GetInfoResponse{ Alias: info.Alias, Color: info.Color, Pubkey: info.Pubkey, @@ -54,7 +55,7 @@ func (svc *Service) HandleGetInfoEvent(ctx context.Context, nip47Request *Nip47R Methods: svc.GetMethods(app), } - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, Result: responsePayload, }, nostr.Tags{}) diff --git a/handle_list_transactions_request.go b/handle_list_transactions_request.go index 20694365..fa883752 100644 --- a/handle_list_transactions_request.go +++ b/handle_list_transactions_request.go @@ -3,14 +3,15 @@ package main import ( "context" + "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/nbd-wtf/go-nostr" "github.com/sirupsen/logrus" ) -func (svc *Service) HandleListTransactionsEvent(ctx context.Context, nip47Request *Nip47Request, requestEvent *RequestEvent, app *App, publishResponse func(*Nip47Response, nostr.Tags)) { +func (svc *Service) HandleListTransactionsEvent(ctx context.Context, nip47Request *nip47.Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*nip47.Response, nostr.Tags)) { - listParams := &Nip47ListTransactionsParams{} + listParams := &nip47.ListTransactionsParams{} resp := svc.decodeNip47Request(nip47Request, requestEvent, app, listParams) if resp != nil { publishResponse(resp, nostr.Tags{}) @@ -23,7 +24,7 @@ func (svc *Service) HandleListTransactionsEvent(ctx context.Context, nip47Reques return } - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ // TODO: log request fields from listParams "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, @@ -37,15 +38,15 @@ func (svc *Service) HandleListTransactionsEvent(ctx context.Context, nip47Reques } transactions, err := svc.lnClient.ListTransactions(ctx, listParams.From, listParams.Until, limit, listParams.Offset, listParams.Unpaid, listParams.Type) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ // TODO: log request fields from listParams "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, }).Infof("Failed to fetch transactions: %v", err) - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, - Error: &Nip47Error{ + Error: &nip47.Error{ Code: nip47.ERROR_INTERNAL, Message: err.Error(), }, @@ -53,11 +54,11 @@ func (svc *Service) HandleListTransactionsEvent(ctx context.Context, nip47Reques return } - responsePayload := &Nip47ListTransactionsResponse{ + responsePayload := &nip47.ListTransactionsResponse{ Transactions: transactions, } - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, Result: responsePayload, }, nostr.Tags{}) diff --git a/handle_lookup_invoice_request.go b/handle_lookup_invoice_request.go index 7c9f14df..352fa862 100644 --- a/handle_lookup_invoice_request.go +++ b/handle_lookup_invoice_request.go @@ -5,15 +5,16 @@ import ( "fmt" "strings" + "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/nbd-wtf/go-nostr" decodepay "github.com/nbd-wtf/ln-decodepay" "github.com/sirupsen/logrus" ) -func (svc *Service) HandleLookupInvoiceEvent(ctx context.Context, nip47Request *Nip47Request, requestEvent *RequestEvent, app *App, publishResponse func(*Nip47Response, nostr.Tags)) { +func (svc *Service) HandleLookupInvoiceEvent(ctx context.Context, nip47Request *nip47.Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*nip47.Response, nostr.Tags)) { - lookupInvoiceParams := &Nip47LookupInvoiceParams{} + lookupInvoiceParams := &nip47.LookupInvoiceParams{} resp := svc.decodeNip47Request(nip47Request, requestEvent, app, lookupInvoiceParams) if resp != nil { publishResponse(resp, nostr.Tags{}) @@ -26,7 +27,7 @@ func (svc *Service) HandleLookupInvoiceEvent(ctx context.Context, nip47Request * return } - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "invoice": lookupInvoiceParams.Invoice, @@ -38,15 +39,15 @@ func (svc *Service) HandleLookupInvoiceEvent(ctx context.Context, nip47Request * if paymentHash == "" { paymentRequest, err := decodepay.Decodepay(strings.ToLower(lookupInvoiceParams.Invoice)) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "invoice": lookupInvoiceParams.Invoice, }).Errorf("Failed to decode bolt11 invoice: %v", err) - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, - Error: &Nip47Error{ + Error: &nip47.Error{ Code: nip47.ERROR_INTERNAL, Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()), }, @@ -58,16 +59,16 @@ func (svc *Service) HandleLookupInvoiceEvent(ctx context.Context, nip47Request * transaction, err := svc.lnClient.LookupInvoice(ctx, paymentHash) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "invoice": lookupInvoiceParams.Invoice, "paymentHash": lookupInvoiceParams.PaymentHash, }).Infof("Failed to lookup invoice: %v", err) - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, - Error: &Nip47Error{ + Error: &nip47.Error{ Code: nip47.ERROR_INTERNAL, Message: err.Error(), }, @@ -75,11 +76,11 @@ func (svc *Service) HandleLookupInvoiceEvent(ctx context.Context, nip47Request * return } - responsePayload := &Nip47LookupInvoiceResponse{ - Nip47Transaction: *transaction, + responsePayload := &nip47.LookupInvoiceResponse{ + Transaction: *transaction, } - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, Result: responsePayload, }, nostr.Tags{}) diff --git a/handle_make_invoice_request.go b/handle_make_invoice_request.go index 7830b51f..8666df1b 100644 --- a/handle_make_invoice_request.go +++ b/handle_make_invoice_request.go @@ -3,14 +3,15 @@ package main import ( "context" + "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/nbd-wtf/go-nostr" "github.com/sirupsen/logrus" ) -func (svc *Service) HandleMakeInvoiceEvent(ctx context.Context, nip47Request *Nip47Request, requestEvent *RequestEvent, app *App, publishResponse func(*Nip47Response, nostr.Tags)) { +func (svc *Service) HandleMakeInvoiceEvent(ctx context.Context, nip47Request *nip47.Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*nip47.Response, nostr.Tags)) { - makeInvoiceParams := &Nip47MakeInvoiceParams{} + makeInvoiceParams := &nip47.MakeInvoiceParams{} resp := svc.decodeNip47Request(nip47Request, requestEvent, app, makeInvoiceParams) if resp != nil { publishResponse(resp, nostr.Tags{}) @@ -23,7 +24,7 @@ func (svc *Service) HandleMakeInvoiceEvent(ctx context.Context, nip47Request *Ni return } - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "amount": makeInvoiceParams.Amount, @@ -39,7 +40,7 @@ func (svc *Service) HandleMakeInvoiceEvent(ctx context.Context, nip47Request *Ni transaction, err := svc.lnClient.MakeInvoice(ctx, makeInvoiceParams.Amount, makeInvoiceParams.Description, makeInvoiceParams.DescriptionHash, expiry) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "amount": makeInvoiceParams.Amount, @@ -48,9 +49,9 @@ func (svc *Service) HandleMakeInvoiceEvent(ctx context.Context, nip47Request *Ni "expiry": makeInvoiceParams.Expiry, }).Infof("Failed to make invoice: %v", err) - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, - Error: &Nip47Error{ + Error: &nip47.Error{ Code: nip47.ERROR_INTERNAL, Message: err.Error(), }, @@ -58,11 +59,11 @@ func (svc *Service) HandleMakeInvoiceEvent(ctx context.Context, nip47Request *Ni return } - responsePayload := &Nip47MakeInvoiceResponse{ - Nip47Transaction: *transaction, + responsePayload := &nip47.MakeInvoiceResponse{ + Transaction: *transaction, } - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, Result: responsePayload, }, nostr.Tags{}) diff --git a/handle_multi_pay_invoice_request.go b/handle_multi_pay_invoice_request.go index 7f4382c6..7fd6e28d 100644 --- a/handle_multi_pay_invoice_request.go +++ b/handle_multi_pay_invoice_request.go @@ -6,6 +6,7 @@ import ( "strings" "sync" + "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/events" "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/nbd-wtf/go-nostr" @@ -14,9 +15,9 @@ import ( ) // TODO: pass a channel instead of publishResponse function -func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Request *Nip47Request, requestEvent *RequestEvent, app *App, publishResponse func(*Nip47Response, nostr.Tags)) { +func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Request *nip47.Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*nip47.Response, nostr.Tags)) { - multiPayParams := &Nip47MultiPayInvoiceParams{} + multiPayParams := &nip47.MultiPayInvoiceParams{} resp := svc.decodeNip47Request(nip47Request, requestEvent, app, multiPayParams) if resp != nil { publishResponse(resp, nostr.Tags{}) @@ -28,14 +29,14 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Request for _, invoiceInfo := range multiPayParams.Invoices { wg.Add(1) // TODO: we should call the handle_payment_request (most of this code is duplicated) - go func(invoiceInfo Nip47MultiPayInvoiceElement) { + go func(invoiceInfo nip47.MultiPayInvoiceElement) { defer wg.Done() bolt11 := invoiceInfo.Invoice // Convert invoice to lowercase string bolt11 = strings.ToLower(bolt11) paymentRequest, err := decodepay.Decodepay(bolt11) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "bolt11": bolt11, @@ -43,9 +44,9 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Request // TODO: Decide what to do if id is empty dTag := []string{"d", invoiceInfo.Id} - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, - Error: &Nip47Error{ + Error: &nip47.Error{ Code: nip47.ERROR_INTERNAL, Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()), }, @@ -65,12 +66,12 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Request return } - payment := Payment{App: *app, RequestEventId: requestEvent.ID, PaymentRequest: bolt11, Amount: uint(paymentRequest.MSatoshi / 1000)} + payment := db.Payment{App: *app, RequestEventId: requestEvent.ID, PaymentRequest: bolt11, Amount: uint(paymentRequest.MSatoshi / 1000)} mu.Lock() insertPaymentResult := svc.db.Create(&payment) mu.Unlock() if insertPaymentResult.Error != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "paymentRequest": bolt11, "invoiceId": invoiceInfo.Id, @@ -78,7 +79,7 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Request return } - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "bolt11": bolt11, @@ -86,13 +87,13 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Request response, err := svc.lnClient.SendPaymentSync(ctx, bolt11) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "bolt11": bolt11, }).Infof("Failed to send payment: %v", err) - svc.EventPublisher.Publish(&events.Event{ + svc.eventPublisher.Publish(&events.Event{ Event: "nwc_payment_failed", Properties: map[string]interface{}{ // "error": fmt.Sprintf("%v", err), @@ -102,9 +103,9 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Request }, }) - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, - Error: &Nip47Error{ + Error: &nip47.Error{ Code: nip47.ERROR_INTERNAL, Message: err.Error(), }, @@ -117,16 +118,16 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Request mu.Lock() svc.db.Save(&payment) mu.Unlock() - svc.EventPublisher.Publish(&events.Event{ + svc.eventPublisher.Publish(&events.Event{ Event: "nwc_payment_succeeded", Properties: map[string]interface{}{ "multi": true, "amount": paymentRequest.MSatoshi / 1000, }, }) - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, - Result: Nip47PayResponse{ + Result: nip47.PayResponse{ Preimage: response.Preimage, FeesPaid: response.Fee, }, diff --git a/handle_multi_pay_keysend_request.go b/handle_multi_pay_keysend_request.go index e4e65c3a..b1acd07a 100644 --- a/handle_multi_pay_keysend_request.go +++ b/handle_multi_pay_keysend_request.go @@ -4,15 +4,16 @@ import ( "context" "sync" + "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/events" "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/nbd-wtf/go-nostr" "github.com/sirupsen/logrus" ) -func (svc *Service) HandleMultiPayKeysendEvent(ctx context.Context, nip47Request *Nip47Request, requestEvent *RequestEvent, app *App, publishResponse func(*Nip47Response, nostr.Tags)) { +func (svc *Service) HandleMultiPayKeysendEvent(ctx context.Context, nip47Request *nip47.Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*nip47.Response, nostr.Tags)) { - multiPayParams := &Nip47MultiPayKeysendParams{} + multiPayParams := &nip47.MultiPayKeysendParams{} resp := svc.decodeNip47Request(nip47Request, requestEvent, app, multiPayParams) if resp != nil { publishResponse(resp, nostr.Tags{}) @@ -23,7 +24,7 @@ func (svc *Service) HandleMultiPayKeysendEvent(ctx context.Context, nip47Request var mu sync.Mutex for _, keysendInfo := range multiPayParams.Keysends { wg.Add(1) - go func(keysendInfo Nip47MultiPayKeysendElement) { + go func(keysendInfo nip47.MultiPayKeysendElement) { defer wg.Done() keysendDTagValue := keysendInfo.Id @@ -38,12 +39,12 @@ func (svc *Service) HandleMultiPayKeysendEvent(ctx context.Context, nip47Request return } - payment := Payment{App: *app, RequestEvent: *requestEvent, Amount: uint(keysendInfo.Amount / 1000)} + payment := db.Payment{App: *app, RequestEvent: *requestEvent, Amount: uint(keysendInfo.Amount / 1000)} mu.Lock() insertPaymentResult := svc.db.Create(&payment) mu.Unlock() if insertPaymentResult.Error != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "recipientPubkey": keysendInfo.Pubkey, "keysendId": keysendInfo.Id, @@ -51,7 +52,7 @@ func (svc *Service) HandleMultiPayKeysendEvent(ctx context.Context, nip47Request return } - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "recipientPubkey": keysendInfo.Pubkey, @@ -59,12 +60,12 @@ func (svc *Service) HandleMultiPayKeysendEvent(ctx context.Context, nip47Request preimage, err := svc.lnClient.SendKeysend(ctx, keysendInfo.Amount, keysendInfo.Pubkey, keysendInfo.Preimage, keysendInfo.TLVRecords) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "recipientPubkey": keysendInfo.Pubkey, }).Infof("Failed to send payment: %v", err) - svc.EventPublisher.Publish(&events.Event{ + svc.eventPublisher.Publish(&events.Event{ Event: "nwc_payment_failed", Properties: map[string]interface{}{ // "error": fmt.Sprintf("%v", err), @@ -74,9 +75,9 @@ func (svc *Service) HandleMultiPayKeysendEvent(ctx context.Context, nip47Request }, }) - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, - Error: &Nip47Error{ + Error: &nip47.Error{ Code: nip47.ERROR_INTERNAL, Message: err.Error(), }, @@ -87,7 +88,7 @@ func (svc *Service) HandleMultiPayKeysendEvent(ctx context.Context, nip47Request mu.Lock() svc.db.Save(&payment) mu.Unlock() - svc.EventPublisher.Publish(&events.Event{ + svc.eventPublisher.Publish(&events.Event{ Event: "nwc_payment_succeeded", Properties: map[string]interface{}{ "keysend": true, @@ -95,9 +96,9 @@ func (svc *Service) HandleMultiPayKeysendEvent(ctx context.Context, nip47Request "amount": keysendInfo.Amount / 1000, }, }) - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, - Result: Nip47PayResponse{ + Result: nip47.PayResponse{ Preimage: preimage, }, }, nostr.Tags{dTag}) diff --git a/handle_pay_keysend_request.go b/handle_pay_keysend_request.go index f63aa8d1..97c867a4 100644 --- a/handle_pay_keysend_request.go +++ b/handle_pay_keysend_request.go @@ -3,15 +3,16 @@ package main import ( "context" + "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/events" "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/nbd-wtf/go-nostr" "github.com/sirupsen/logrus" ) -func (svc *Service) HandlePayKeysendEvent(ctx context.Context, nip47Request *Nip47Request, requestEvent *RequestEvent, app *App, publishResponse func(*Nip47Response, nostr.Tags)) { +func (svc *Service) HandlePayKeysendEvent(ctx context.Context, nip47Request *nip47.Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*nip47.Response, nostr.Tags)) { - payParams := &Nip47KeysendParams{} + payParams := &nip47.KeysendParams{} resp := svc.decodeNip47Request(nip47Request, requestEvent, app, payParams) if resp != nil { publishResponse(resp, nostr.Tags{}) @@ -24,12 +25,12 @@ func (svc *Service) HandlePayKeysendEvent(ctx context.Context, nip47Request *Nip return } - payment := Payment{App: *app, RequestEvent: *requestEvent, Amount: uint(payParams.Amount / 1000)} + payment := db.Payment{App: *app, RequestEvent: *requestEvent, Amount: uint(payParams.Amount / 1000)} err := svc.db.Create(&payment).Error if err != nil { - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, - Error: &Nip47Error{ + Error: &nip47.Error{ Code: nip47.ERROR_INTERNAL, Message: err.Error(), }, @@ -37,7 +38,7 @@ func (svc *Service) HandlePayKeysendEvent(ctx context.Context, nip47Request *Nip return } - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "senderPubkey": payParams.Pubkey, @@ -45,12 +46,12 @@ func (svc *Service) HandlePayKeysendEvent(ctx context.Context, nip47Request *Nip preimage, err := svc.lnClient.SendKeysend(ctx, payParams.Amount, payParams.Pubkey, payParams.Preimage, payParams.TLVRecords) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "recipientPubkey": payParams.Pubkey, }).Infof("Failed to send payment: %v", err) - svc.EventPublisher.Publish(&events.Event{ + svc.eventPublisher.Publish(&events.Event{ Event: "nwc_payment_failed", Properties: map[string]interface{}{ // "error": fmt.Sprintf("%v", err), @@ -58,9 +59,9 @@ func (svc *Service) HandlePayKeysendEvent(ctx context.Context, nip47Request *Nip "amount": payParams.Amount / 1000, }, }) - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, - Error: &Nip47Error{ + Error: &nip47.Error{ Code: nip47.ERROR_INTERNAL, Message: err.Error(), }, @@ -69,16 +70,16 @@ func (svc *Service) HandlePayKeysendEvent(ctx context.Context, nip47Request *Nip } payment.Preimage = &preimage svc.db.Save(&payment) - svc.EventPublisher.Publish(&events.Event{ + svc.eventPublisher.Publish(&events.Event{ Event: "nwc_payment_succeeded", Properties: map[string]interface{}{ "keysend": true, "amount": payParams.Amount / 1000, }, }) - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, - Result: Nip47PayResponse{ + Result: nip47.PayResponse{ Preimage: preimage, }, }, nostr.Tags{}) diff --git a/handle_payment_request.go b/handle_payment_request.go index bbe6600a..16afba69 100644 --- a/handle_payment_request.go +++ b/handle_payment_request.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/events" "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/nbd-wtf/go-nostr" @@ -12,9 +13,9 @@ import ( "github.com/sirupsen/logrus" ) -func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, nip47Request *Nip47Request, requestEvent *RequestEvent, app *App, publishResponse func(*Nip47Response, nostr.Tags)) { +func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, nip47Request *nip47.Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*nip47.Response, nostr.Tags)) { - payParams := &Nip47PayParams{} + payParams := &nip47.PayParams{} resp := svc.decodeNip47Request(nip47Request, requestEvent, app, payParams) if resp != nil { publishResponse(resp, nostr.Tags{}) @@ -26,15 +27,15 @@ func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, nip47Request *Nip bolt11 = strings.ToLower(bolt11) paymentRequest, err := decodepay.Decodepay(bolt11) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "bolt11": bolt11, }).Errorf("Failed to decode bolt11 invoice: %v", err) - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, - Error: &Nip47Error{ + Error: &nip47.Error{ Code: nip47.ERROR_INTERNAL, Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()), }, @@ -48,12 +49,12 @@ func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, nip47Request *Nip return } - payment := Payment{App: *app, RequestEvent: *requestEvent, PaymentRequest: bolt11, Amount: uint(paymentRequest.MSatoshi / 1000)} + payment := db.Payment{App: *app, RequestEvent: *requestEvent, PaymentRequest: bolt11, Amount: uint(paymentRequest.MSatoshi / 1000)} err = svc.db.Create(&payment).Error if err != nil { - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, - Error: &Nip47Error{ + Error: &nip47.Error{ Code: nip47.ERROR_INTERNAL, Message: err.Error(), }, @@ -61,7 +62,7 @@ func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, nip47Request *Nip return } - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "bolt11": bolt11, @@ -69,12 +70,12 @@ func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, nip47Request *Nip response, err := svc.lnClient.SendPaymentSync(ctx, bolt11) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "bolt11": bolt11, }).Infof("Failed to send payment: %v", err) - svc.EventPublisher.Publish(&events.Event{ + svc.eventPublisher.Publish(&events.Event{ Event: "nwc_payment_failed", Properties: map[string]interface{}{ // "error": fmt.Sprintf("%v", err), @@ -82,9 +83,9 @@ func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, nip47Request *Nip "amount": paymentRequest.MSatoshi / 1000, }, }) - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, - Error: &Nip47Error{ + Error: &nip47.Error{ Code: nip47.ERROR_INTERNAL, Message: err.Error(), }, @@ -95,7 +96,7 @@ func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, nip47Request *Nip // TODO: save payment fee svc.db.Save(&payment) - svc.EventPublisher.Publish(&events.Event{ + svc.eventPublisher.Publish(&events.Event{ Event: "nwc_payment_succeeded", Properties: map[string]interface{}{ "bolt11": bolt11, @@ -103,9 +104,9 @@ func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, nip47Request *Nip }, }) - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, - Result: Nip47PayResponse{ + Result: nip47.PayResponse{ Preimage: response.Preimage, FeesPaid: response.Fee, }, diff --git a/handle_sign_message_request.go b/handle_sign_message_request.go index fa9e1183..bf4e22eb 100644 --- a/handle_sign_message_request.go +++ b/handle_sign_message_request.go @@ -3,13 +3,14 @@ package main import ( "context" + "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/nbd-wtf/go-nostr" "github.com/sirupsen/logrus" ) -func (svc *Service) HandleSignMessageEvent(ctx context.Context, nip47Request *Nip47Request, requestEvent *RequestEvent, app *App, publishResponse func(*Nip47Response, nostr.Tags)) { - signParams := &Nip47SignMessageParams{} +func (svc *Service) HandleSignMessageEvent(ctx context.Context, nip47Request *nip47.Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*nip47.Response, nostr.Tags)) { + signParams := &nip47.SignMessageParams{} resp := svc.decodeNip47Request(nip47Request, requestEvent, app, signParams) if resp != nil { publishResponse(resp, nostr.Tags{}) @@ -22,20 +23,20 @@ func (svc *Service) HandleSignMessageEvent(ctx context.Context, nip47Request *Ni return } - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, }).Info("Signing message") signature, err := svc.lnClient.SignMessage(ctx, signParams.Message) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, }).Infof("Failed to sign message: %v", err) - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, - Error: &Nip47Error{ + Error: &nip47.Error{ Code: nip47.ERROR_INTERNAL, Message: err.Error(), }, @@ -43,12 +44,12 @@ func (svc *Service) HandleSignMessageEvent(ctx context.Context, nip47Request *Ni return } - responsePayload := Nip47SignMessageResponse{ + responsePayload := nip47.SignMessageResponse{ Message: signParams.Message, Signature: signature, } - publishResponse(&Nip47Response{ + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, Result: responsePayload, }, nostr.Tags{}) diff --git a/http_service.go b/http/http_service.go similarity index 76% rename from http_service.go rename to http/http_service.go index 6bd4d58e..1901703a 100644 --- a/http_service.go +++ b/http/http_service.go @@ -1,4 +1,4 @@ -package main +package http import ( "bytes" @@ -12,20 +12,27 @@ import ( "github.com/labstack/echo-contrib/session" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" + "github.com/sirupsen/logrus" "gorm.io/gorm" "github.com/getAlby/nostr-wallet-connect/alby" + "github.com/getAlby/nostr-wallet-connect/config" + "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/events" - models "github.com/getAlby/nostr-wallet-connect/models/http" + "github.com/getAlby/nostr-wallet-connect/lsp" + "github.com/getAlby/nostr-wallet-connect/service" + "github.com/getAlby/nostr-wallet-connect/api" "github.com/getAlby/nostr-wallet-connect/frontend" - "github.com/getAlby/nostr-wallet-connect/models/api" ) type HttpService struct { - svc *Service - api *API - albyHttpSvc *alby.AlbyHttpService + api api.API + albyHttpSvc *alby.AlbyHttpService + cfg config.Config + db *gorm.DB + logger *logrus.Logger + eventPublisher events.EventPublisher } const ( @@ -33,11 +40,15 @@ const ( sessionCookieAuthKey = "authenticated" ) -func NewHttpService(svc *Service) *HttpService { +func NewHttpService(svc service.Service, logger *logrus.Logger, db *gorm.DB, eventPublisher events.EventPublisher) *HttpService { + return &HttpService{ - svc: svc, - api: NewAPI(svc), - albyHttpSvc: alby.NewAlbyHttpService(svc.AlbyOAuthSvc, svc.Logger), + api: api.NewAPI(svc, logger, db), + albyHttpSvc: alby.NewAlbyHttpService(svc.GetAlbyOAuthSvc(), logger, svc.GetConfig().GetEnv()), + cfg: svc.GetConfig(), + db: db, + logger: logger, + eventPublisher: eventPublisher, } } @@ -58,7 +69,7 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) { e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ TokenLookup: "header:X-CSRF-Token", })) - e.Use(session.Middleware(sessions.NewCookieStore([]byte(httpSvc.svc.cfg.CookieSecret)))) + e.Use(session.Middleware(sessions.NewCookieStore([]byte(httpSvc.cfg.GetCookieSecret())))) authMiddleware := httpSvc.validateUserMiddleware e.GET("/api/apps", httpSvc.appsListHandler, authMiddleware) @@ -117,7 +128,7 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) { func (httpSvc *HttpService) csrfHandler(c echo.Context) error { csrf, _ := c.Get(middleware.DefaultCSRFConfig.ContextKey).(string) if csrf == "" { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: "CSRF token not available", }) } @@ -127,7 +138,7 @@ func (httpSvc *HttpService) csrfHandler(c echo.Context) error { func (httpSvc *HttpService) infoHandler(c echo.Context) error { responseBody, err := httpSvc.api.GetInfo(c.Request().Context()) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: err.Error(), }) } @@ -143,14 +154,14 @@ func (httpSvc *HttpService) encryptedMnemonicHandler(c echo.Context) error { func (httpSvc *HttpService) backupReminderHandler(c echo.Context) error { var backupReminderRequest api.BackupReminderRequest if err := c.Bind(&backupReminderRequest); err != nil { - return c.JSON(http.StatusBadRequest, models.ErrorResponse{ + return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: fmt.Sprintf("Bad request: %s", err.Error()), }) } err := httpSvc.api.SetNextBackupReminder(&backupReminderRequest) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to store backup reminder: %s", err.Error()), }) } @@ -161,14 +172,14 @@ func (httpSvc *HttpService) backupReminderHandler(c echo.Context) error { func (httpSvc *HttpService) startHandler(c echo.Context) error { var startRequest api.StartRequest if err := c.Bind(&startRequest); err != nil { - return c.JSON(http.StatusBadRequest, models.ErrorResponse{ + return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: fmt.Sprintf("Bad request: %s", err.Error()), }) } err := httpSvc.api.Start(&startRequest) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to start node: %s", err.Error()), }) } @@ -176,7 +187,7 @@ func (httpSvc *HttpService) startHandler(c echo.Context) error { err = httpSvc.saveSessionCookie(c) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to save session: %s", err.Error()), }) } @@ -187,13 +198,13 @@ func (httpSvc *HttpService) startHandler(c echo.Context) error { func (httpSvc *HttpService) unlockHandler(c echo.Context) error { var unlockRequest api.UnlockRequest if err := c.Bind(&unlockRequest); err != nil { - return c.JSON(http.StatusBadRequest, models.ErrorResponse{ + return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: fmt.Sprintf("Bad request: %s", err.Error()), }) } - if !httpSvc.svc.cfg.CheckUnlockPassword(unlockRequest.UnlockPassword) { - return c.JSON(http.StatusUnauthorized, models.ErrorResponse{ + if !httpSvc.cfg.CheckUnlockPassword(unlockRequest.UnlockPassword) { + return c.JSON(http.StatusUnauthorized, ErrorResponse{ Message: "Invalid password", }) } @@ -201,12 +212,12 @@ func (httpSvc *HttpService) unlockHandler(c echo.Context) error { err := httpSvc.saveSessionCookie(c) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to save session: %s", err.Error()), }) } - httpSvc.svc.EventPublisher.Publish(&events.Event{ + httpSvc.eventPublisher.Publish(&events.Event{ Event: "nwc_unlocked", }) @@ -216,14 +227,14 @@ func (httpSvc *HttpService) unlockHandler(c echo.Context) error { func (httpSvc *HttpService) changeUnlockPasswordHandler(c echo.Context) error { var changeUnlockPasswordRequest api.ChangeUnlockPasswordRequest if err := c.Bind(&changeUnlockPasswordRequest); err != nil { - return c.JSON(http.StatusBadRequest, models.ErrorResponse{ + return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: fmt.Sprintf("Bad request: %s", err.Error()), }) } err := httpSvc.api.ChangeUnlockPassword(&changeUnlockPasswordRequest) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to change unlock password: %s", err.Error()), }) } @@ -246,7 +257,7 @@ func (httpSvc *HttpService) saveSessionCookie(c echo.Context) error { sess.Values[sessionCookieAuthKey] = true err := sess.Save(c.Request(), c.Response()) if err != nil { - httpSvc.svc.Logger.WithError(err).Error("Failed to save session") + httpSvc.logger.WithError(err).Error("Failed to save session") } return err } @@ -254,13 +265,13 @@ func (httpSvc *HttpService) saveSessionCookie(c echo.Context) error { func (httpSvc *HttpService) logoutHandler(c echo.Context) error { sess, err := session.Get(sessionCookieName, c) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: "Failed to get session", }) } sess.Options.MaxAge = -1 if err := sess.Save(c.Request(), c.Response()); err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: "Failed to save session", }) } @@ -273,7 +284,7 @@ func (httpSvc *HttpService) channelsListHandler(c echo.Context) error { channels, err := httpSvc.api.ListChannels(ctx) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: err.Error(), }) } @@ -287,7 +298,7 @@ func (httpSvc *HttpService) channelPeerSuggestionsHandler(c echo.Context) error suggestions, err := httpSvc.api.GetChannelPeerSuggestions(ctx) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: err.Error(), }) } @@ -298,15 +309,15 @@ func (httpSvc *HttpService) channelPeerSuggestionsHandler(c echo.Context) error func (httpSvc *HttpService) resetRouterHandler(c echo.Context) error { var resetRouterRequest api.ResetRouterRequest if err := c.Bind(&resetRouterRequest); err != nil { - return c.JSON(http.StatusBadRequest, models.ErrorResponse{ + return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: fmt.Sprintf("Bad request: %s", err.Error()), }) } - err := httpSvc.api.ResetRouter(resetRouterRequest.Key, true) + err := httpSvc.api.ResetRouter(resetRouterRequest.Key) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: err.Error(), }) } @@ -319,7 +330,7 @@ func (httpSvc *HttpService) stopHandler(c echo.Context) error { err := httpSvc.api.Stop() if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: err.Error(), }) } @@ -333,7 +344,7 @@ func (httpSvc *HttpService) nodeConnectionInfoHandler(c echo.Context) error { info, err := httpSvc.api.GetNodeConnectionInfo(ctx) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: err.Error(), }) } @@ -347,7 +358,7 @@ func (httpSvc *HttpService) nodeStatusHandler(c echo.Context) error { info, err := httpSvc.api.GetNodeStatus(ctx) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: err.Error(), }) } @@ -361,7 +372,7 @@ func (httpSvc *HttpService) nodeNetworkGraphHandler(c echo.Context) error { info, err := httpSvc.api.GetNetworkGraph(nodeIds) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: err.Error(), }) } @@ -375,7 +386,7 @@ func (httpSvc *HttpService) balancesHandler(c echo.Context) error { balances, err := httpSvc.api.GetBalances(ctx) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: err.Error(), }) } @@ -392,15 +403,15 @@ func (httpSvc *HttpService) walletSyncHandler(c echo.Context) error { func (httpSvc *HttpService) mempoolApiHandler(c echo.Context) error { endpoint := c.QueryParam("endpoint") if endpoint == "" { - return c.JSON(http.StatusBadRequest, models.ErrorResponse{ + return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: "Invalid pubkey parameter", }) } response, err := httpSvc.api.RequestMempoolApi(endpoint) if err != nil { - httpSvc.svc.Logger.WithField("endpoint", endpoint).WithError(err).Error("Failed to request mempool API") - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + httpSvc.logger.WithField("endpoint", endpoint).WithError(err).Error("Failed to request mempool API") + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to request mempool API: %s", err.Error()), }) } @@ -409,9 +420,9 @@ func (httpSvc *HttpService) mempoolApiHandler(c echo.Context) error { } func (httpSvc *HttpService) listPeers(c echo.Context) error { - peers, err := httpSvc.api.ListPeers(httpSvc.svc.ctx) + peers, err := httpSvc.api.ListPeers(c.Request().Context()) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to list peers: %s", err.Error()), }) } @@ -424,7 +435,7 @@ func (httpSvc *HttpService) connectPeerHandler(c echo.Context) error { var connectPeerRequest api.ConnectPeerRequest if err := c.Bind(&connectPeerRequest); err != nil { - return c.JSON(http.StatusBadRequest, models.ErrorResponse{ + return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: fmt.Sprintf("Bad request: %s", err.Error()), }) } @@ -432,7 +443,7 @@ func (httpSvc *HttpService) connectPeerHandler(c echo.Context) error { err := httpSvc.api.ConnectPeer(ctx, &connectPeerRequest) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to connect peer: %s", err.Error()), }) } @@ -445,7 +456,7 @@ func (httpSvc *HttpService) openChannelHandler(c echo.Context) error { var openChannelRequest api.OpenChannelRequest if err := c.Bind(&openChannelRequest); err != nil { - return c.JSON(http.StatusBadRequest, models.ErrorResponse{ + return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: fmt.Sprintf("Bad request: %s", err.Error()), }) } @@ -453,7 +464,7 @@ func (httpSvc *HttpService) openChannelHandler(c echo.Context) error { openChannelResponse, err := httpSvc.api.OpenChannel(ctx, &openChannelRequest) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to open channel: %s", err.Error()), }) } @@ -467,7 +478,7 @@ func (httpSvc *HttpService) closeChannelHandler(c echo.Context) error { closeChannelResponse, err := httpSvc.api.CloseChannel(ctx, c.Param("peerId"), c.Param("channelId"), c.QueryParam("force") == "true") if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to close channel: %s", err.Error()), }) } @@ -478,17 +489,17 @@ func (httpSvc *HttpService) closeChannelHandler(c echo.Context) error { func (httpSvc *HttpService) newInstantChannelInvoiceHandler(c echo.Context) error { ctx := c.Request().Context() - var newWrappedInvoiceRequest api.NewInstantChannelInvoiceRequest + var newWrappedInvoiceRequest lsp.NewInstantChannelInvoiceRequest if err := c.Bind(&newWrappedInvoiceRequest); err != nil { - return c.JSON(http.StatusBadRequest, models.ErrorResponse{ + return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: fmt.Sprintf("Bad request: %s", err.Error()), }) } - newWrappedInvoiceResponse, err := httpSvc.api.NewInstantChannelInvoice(ctx, &newWrappedInvoiceRequest) + newWrappedInvoiceResponse, err := httpSvc.api.GetLSPService().NewInstantChannelInvoice(ctx, &newWrappedInvoiceRequest) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to request wrapped invoice: %s", err.Error()), }) } @@ -502,7 +513,7 @@ func (httpSvc *HttpService) newOnchainAddressHandler(c echo.Context) error { newAddressResponse, err := httpSvc.api.GetNewOnchainAddress(ctx) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to request new onchain address: %s", err.Error()), }) } @@ -515,7 +526,7 @@ func (httpSvc *HttpService) redeemOnchainFundsHandler(c echo.Context) error { var redeemOnchainFundsRequest api.RedeemOnchainFundsRequest if err := c.Bind(&redeemOnchainFundsRequest); err != nil { - return c.JSON(http.StatusBadRequest, models.ErrorResponse{ + return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: fmt.Sprintf("Bad request: %s", err.Error()), }) } @@ -523,7 +534,7 @@ func (httpSvc *HttpService) redeemOnchainFundsHandler(c echo.Context) error { redeemOnchainFundsResponse, err := httpSvc.api.RedeemOnchainFunds(ctx, redeemOnchainFundsRequest.ToAddress) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to redeem onchain funds: %s", err.Error()), }) } @@ -536,7 +547,7 @@ func (httpSvc *HttpService) signMessageHandler(c echo.Context) error { var signMessageRequest api.SignMessageRequest if err := c.Bind(&signMessageRequest); err != nil { - return c.JSON(http.StatusBadRequest, models.ErrorResponse{ + return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: fmt.Sprintf("Bad request: %s", err.Error()), }) } @@ -544,7 +555,7 @@ func (httpSvc *HttpService) signMessageHandler(c echo.Context) error { signMessageResponse, err := httpSvc.api.SignMessage(ctx, signMessageRequest.Message) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to sign messae: %s", err.Error()), }) } @@ -555,7 +566,7 @@ func (httpSvc *HttpService) appsListHandler(c echo.Context) error { apps, err := httpSvc.api.ListApps() if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: err.Error(), }) } @@ -564,11 +575,11 @@ func (httpSvc *HttpService) appsListHandler(c echo.Context) error { } func (httpSvc *HttpService) appsShowHandler(c echo.Context) error { - app := App{} - findResult := httpSvc.svc.db.Where("nostr_pubkey = ?", c.Param("pubkey")).First(&app) + app := db.App{} + findResult := httpSvc.db.Where("nostr_pubkey = ?", c.Param("pubkey")).First(&app) if findResult.RowsAffected == 0 { - return c.JSON(http.StatusNotFound, models.ErrorResponse{ + return c.JSON(http.StatusNotFound, ErrorResponse{ Message: "App does not exist", }) } @@ -581,16 +592,16 @@ func (httpSvc *HttpService) appsShowHandler(c echo.Context) error { func (httpSvc *HttpService) appsUpdateHandler(c echo.Context) error { var requestData api.UpdateAppRequest if err := c.Bind(&requestData); err != nil { - return c.JSON(http.StatusBadRequest, models.ErrorResponse{ + return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: fmt.Sprintf("Bad request: %s", err.Error()), }) } - app := App{} - findResult := httpSvc.svc.db.Where("nostr_pubkey = ?", c.Param("pubkey")).First(&app) + app := db.App{} + findResult := httpSvc.db.Where("nostr_pubkey = ?", c.Param("pubkey")).First(&app) if findResult.RowsAffected == 0 { - return c.JSON(http.StatusNotFound, models.ErrorResponse{ + return c.JSON(http.StatusNotFound, ErrorResponse{ Message: "App does not exist", }) } @@ -598,8 +609,8 @@ func (httpSvc *HttpService) appsUpdateHandler(c echo.Context) error { err := httpSvc.api.UpdateApp(&app, &requestData) if err != nil { - httpSvc.svc.Logger.WithError(err).Error("Failed to update app") - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + httpSvc.logger.WithError(err).Error("Failed to update app") + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to update app: %v", err), }) } @@ -610,25 +621,25 @@ func (httpSvc *HttpService) appsUpdateHandler(c echo.Context) error { func (httpSvc *HttpService) appsDeleteHandler(c echo.Context) error { pubkey := c.Param("pubkey") if pubkey == "" { - return c.JSON(http.StatusBadRequest, models.ErrorResponse{ + return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: "Invalid pubkey parameter", }) } - app := App{} - result := httpSvc.svc.db.Where("nostr_pubkey = ?", pubkey).First(&app) + app := db.App{} + result := httpSvc.db.Where("nostr_pubkey = ?", pubkey).First(&app) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { - return c.JSON(http.StatusNotFound, models.ErrorResponse{ + return c.JSON(http.StatusNotFound, ErrorResponse{ Message: "App not found", }) } - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: "Failed to fetch app", }) } if err := httpSvc.api.DeleteApp(&app); err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: "Failed to delete app", }) } @@ -638,7 +649,7 @@ func (httpSvc *HttpService) appsDeleteHandler(c echo.Context) error { func (httpSvc *HttpService) appsCreateHandler(c echo.Context) error { var requestData api.CreateAppRequest if err := c.Bind(&requestData); err != nil { - return c.JSON(http.StatusBadRequest, models.ErrorResponse{ + return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: fmt.Sprintf("Bad request: %s", err.Error()), }) } @@ -646,8 +657,8 @@ func (httpSvc *HttpService) appsCreateHandler(c echo.Context) error { responseBody, err := httpSvc.api.CreateApp(&requestData) if err != nil { - httpSvc.svc.Logger.WithField("requestData", requestData).WithError(err).Error("Failed to save app") - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + httpSvc.logger.WithField("requestData", requestData).WithError(err).Error("Failed to save app") + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to save app: %v", err), }) } @@ -658,14 +669,14 @@ func (httpSvc *HttpService) appsCreateHandler(c echo.Context) error { func (httpSvc *HttpService) setupHandler(c echo.Context) error { var setupRequest api.SetupRequest if err := c.Bind(&setupRequest); err != nil { - return c.JSON(http.StatusBadRequest, models.ErrorResponse{ + return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: fmt.Sprintf("Bad request: %s", err.Error()), }) } err := httpSvc.api.Setup(c.Request().Context(), &setupRequest) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to setup node: %s", err.Error()), }) } @@ -676,14 +687,14 @@ func (httpSvc *HttpService) setupHandler(c echo.Context) error { func (httpSvc *HttpService) sendPaymentProbesHandler(c echo.Context) error { var sendPaymentProbesRequest api.SendPaymentProbesRequest if err := c.Bind(&sendPaymentProbesRequest); err != nil { - return c.JSON(http.StatusBadRequest, models.ErrorResponse{ + return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: fmt.Sprintf("Bad request: %s", err.Error()), }) } sendPaymentProbesResponse, err := httpSvc.api.SendPaymentProbes(c.Request().Context(), &sendPaymentProbesRequest) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to send payment probes: %v", err), }) } @@ -694,14 +705,14 @@ func (httpSvc *HttpService) sendPaymentProbesHandler(c echo.Context) error { func (httpSvc *HttpService) sendSpontaneousPaymentProbesHandler(c echo.Context) error { var sendSpontaneousPaymentProbesRequest api.SendSpontaneousPaymentProbesRequest if err := c.Bind(&sendSpontaneousPaymentProbesRequest); err != nil { - return c.JSON(http.StatusBadRequest, models.ErrorResponse{ + return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: fmt.Sprintf("Bad request: %s", err.Error()), }) } sendSpontaneousPaymentProbesResponse, err := httpSvc.api.SendSpontaneousPaymentProbes(c.Request().Context(), &sendSpontaneousPaymentProbesRequest) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to send spontaneous payment probes: %v", err), }) } @@ -712,21 +723,21 @@ func (httpSvc *HttpService) sendSpontaneousPaymentProbesHandler(c echo.Context) func (httpSvc *HttpService) getLogOutputHandler(c echo.Context) error { var getLogRequest api.GetLogOutputRequest if err := c.Bind(&getLogRequest); err != nil { - return c.JSON(http.StatusBadRequest, models.ErrorResponse{ + return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: fmt.Sprintf("Bad request: %s", err.Error()), }) } logType := c.Param("type") if logType != api.LogTypeNode && logType != api.LogTypeApp { - return c.JSON(http.StatusBadRequest, models.ErrorResponse{ + return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: fmt.Sprintf("Invalid log type parameter: '%s'", logType), }) } getLogResponse, err := httpSvc.api.GetLogOutput(c.Request().Context(), logType, &getLogRequest) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to get log output: %v", err), }) } @@ -737,19 +748,19 @@ func (httpSvc *HttpService) getLogOutputHandler(c echo.Context) error { func (httpSvc *HttpService) createBackupHandler(c echo.Context) error { var backupRequest api.BasicBackupRequest if err := c.Bind(&backupRequest); err != nil { - return c.JSON(http.StatusBadRequest, models.ErrorResponse{ + return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: fmt.Sprintf("Bad request: %s", err.Error()), }) } - if !httpSvc.svc.cfg.CheckUnlockPassword(backupRequest.UnlockPassword) { - return c.JSON(http.StatusUnauthorized, models.ErrorResponse{ + if !httpSvc.cfg.CheckUnlockPassword(backupRequest.UnlockPassword) { + return c.JSON(http.StatusUnauthorized, ErrorResponse{ Message: "Invalid password", }) } var buffer bytes.Buffer - err := httpSvc.api.CreateBackup(&backupRequest, &buffer) + err := httpSvc.api.GetBackupService().CreateBackup(backupRequest.UnlockPassword, &buffer) if err != nil { return c.String(500, fmt.Sprintf("Failed to create backup: %v", err)) } @@ -774,22 +785,22 @@ func (httpSvc *HttpService) restoreBackupHandler(c echo.Context) error { fileHeader, err := c.FormFile("backup") if err != nil { - return c.JSON(http.StatusBadRequest, models.ErrorResponse{ + return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: fmt.Sprintf("Failed to get backup file header: %v", err), }) } file, err := fileHeader.Open() if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to open backup file: %v", err), }) } defer file.Close() - err = httpSvc.api.RestoreBackup(password, file) + err = httpSvc.api.GetBackupService().RestoreBackup(password, file) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to restore backup: %v", err), }) } diff --git a/models/http/http.go b/http/models.go similarity index 70% rename from models/http/http.go rename to http/models.go index b93c83e7..0f58304c 100644 --- a/models/http/http.go +++ b/http/models.go @@ -1,4 +1,3 @@ -// TODO: move to http/models.go package http type ErrorResponse struct { diff --git a/breez.go b/lnclient/breez/breez.go similarity index 95% rename from breez.go rename to lnclient/breez/breez.go index cb48bef9..7ce596c2 100644 --- a/breez.go +++ b/lnclient/breez/breez.go @@ -1,6 +1,6 @@ //go:build !skip_breez -package main +package breez import ( "context" @@ -16,7 +16,8 @@ import ( decodepay "github.com/nbd-wtf/ln-decodepay" "github.com/sirupsen/logrus" - "github.com/getAlby/nostr-wallet-connect/models/lnclient" + "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/nip47" ) type BreezService struct { @@ -102,7 +103,7 @@ func (bs *BreezService) Shutdown() error { return bs.svc.Disconnect() } -func (bs *BreezService) SendPaymentSync(ctx context.Context, payReq string) (*lnclient.Nip47PayInvoiceResponse, error) { +func (bs *BreezService) SendPaymentSync(ctx context.Context, payReq string) (*lnclient.PayInvoiceResponse, error) { sendPaymentRequest := breez_sdk.SendPaymentRequest{ Bolt11: payReq, } @@ -114,7 +115,7 @@ func (bs *BreezService) SendPaymentSync(ctx context.Context, payReq string) (*ln if resp.Payment.Details != nil { lnDetails, _ = resp.Payment.Details.(breez_sdk.PaymentDetailsLn) } - return &lnclient.Nip47PayInvoiceResponse{ + return &lnclient.PayInvoiceResponse{ Preimage: lnDetails.Data.PaymentPreimage, }, nil @@ -153,7 +154,7 @@ func (bs *BreezService) GetBalance(ctx context.Context) (balance int64, err erro return int64(info.MaxPayableMsat), nil } -func (bs *BreezService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *Nip47Transaction, err error) { +func (bs *BreezService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *nip47.Transaction, err error) { expiry32 := uint32(expiry) receivePaymentRequest := breez_sdk.ReceivePaymentRequest{ // amount provided in msat @@ -166,7 +167,7 @@ func (bs *BreezService) MakeInvoice(ctx context.Context, amount int64, descripti return nil, err } - tx := &Nip47Transaction{ + tx := &nip47.Transaction{ Type: "incoming", Invoice: resp.LnInvoice.Bolt11, Preimage: hex.EncodeToString(resp.LnInvoice.PaymentSecret), @@ -191,7 +192,7 @@ func (bs *BreezService) MakeInvoice(ctx context.Context, amount int64, descripti return tx, nil } -func (bs *BreezService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *Nip47Transaction, err error) { +func (bs *BreezService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *nip47.Transaction, err error) { log.Printf("p: %v", paymentHash) payment, err := bs.svc.PaymentByHash(paymentHash) if err != nil { @@ -209,7 +210,7 @@ func (bs *BreezService) LookupInvoice(ctx context.Context, paymentHash string) ( } } -func (bs *BreezService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []Nip47Transaction, err error) { +func (bs *BreezService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []nip47.Transaction, err error) { request := breez_sdk.ListPaymentsRequest{} if limit > 0 { @@ -234,7 +235,7 @@ func (bs *BreezService) ListTransactions(ctx context.Context, from, until, limit return nil, err } - transactions = []Nip47Transaction{} + transactions = []nip47.Transaction{} for _, payment := range payments { if payment.PaymentType != breez_sdk.PaymentTypeReceived && payment.PaymentType != breez_sdk.PaymentTypeSent { // skip other types of payments for now @@ -280,7 +281,7 @@ func (bs *BreezService) CloseChannel(ctx context.Context, closeChannelRequest *l return nil, nil } -func breezPaymentToTransaction(payment *breez_sdk.Payment) (*Nip47Transaction, error) { +func breezPaymentToTransaction(payment *breez_sdk.Payment) (*nip47.Transaction, error) { var lnDetails breez_sdk.PaymentDetailsLn if payment.Details != nil { lnDetails, _ = payment.Details.(breez_sdk.PaymentDetailsLn) @@ -312,7 +313,7 @@ func breezPaymentToTransaction(payment *breez_sdk.Payment) (*Nip47Transaction, e descriptionHash = paymentRequest.DescriptionHash } - tx := &Nip47Transaction{ + tx := &nip47.Transaction{ Type: txType, Invoice: lnDetails.Data.Bolt11, Preimage: lnDetails.Data.PaymentPreimage, @@ -463,3 +464,5 @@ func (bs *BreezService) GetStorageDir() (string, error) { func (bs *BreezService) GetNetworkGraph(nodeIds []string) (lnclient.NetworkGraphResponse, error) { return nil, nil } + +func (bs *BreezService) UpdateLastWalletSyncRequest() {} diff --git a/breez_stub.go b/lnclient/breez/breez_stub.go similarity index 77% rename from breez_stub.go rename to lnclient/breez/breez_stub.go index a28bd4cd..de443cb9 100644 --- a/breez_stub.go +++ b/lnclient/breez/breez_stub.go @@ -1,11 +1,11 @@ //go:build skip_breez -package main +package breez import ( "github.com/sirupsen/logrus" - "github.com/getAlby/nostr-wallet-connect/models/lnclient" + "github.com/getAlby/nostr-wallet-connect/lnclient" ) func NewBreezService(logger *logrus.Logger, mnemonic, apiKey, inviteCode, workDir string) (result lnclient.LNClient, err error) { diff --git a/greenlight.go b/lnclient/greenlight/greenlight.go similarity index 85% rename from greenlight.go rename to lnclient/greenlight/greenlight.go index b3ba6b76..43c44c5e 100644 --- a/greenlight.go +++ b/lnclient/greenlight/greenlight.go @@ -1,4 +1,4 @@ -package main +package greenlight import ( "context" @@ -19,18 +19,20 @@ import ( decodepay "github.com/nbd-wtf/ln-decodepay" "github.com/sirupsen/logrus" - "github.com/getAlby/nostr-wallet-connect/models/lnclient" + "github.com/getAlby/nostr-wallet-connect/config" + "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/nip47" ) type GreenlightService struct { workdir string client *glalby.BlockingGreenlightAlbyClient - svc *Service + logger *logrus.Logger } const DEVICE_CREDENTIALS_KEY = "GreenlightCreds" -func NewGreenlightService(svc *Service, mnemonic, inviteCode, workDir, encryptionKey string) (result lnclient.LNClient, err error) { +func NewGreenlightService(cfg config.Config, logger *logrus.Logger, mnemonic, inviteCode, workDir, encryptionKey string) (result lnclient.LNClient, err error) { if mnemonic == "" || inviteCode == "" || workDir == "" { return nil, errors.New("one or more required greenlight configuration are missing") } @@ -44,35 +46,35 @@ func NewGreenlightService(svc *Service, mnemonic, inviteCode, workDir, encryptio } var credentials *glalby.GreenlightCredentials - existingDeviceCreds, _ := svc.cfg.Get(DEVICE_CREDENTIALS_KEY, encryptionKey) + existingDeviceCreds, _ := cfg.Get(DEVICE_CREDENTIALS_KEY, encryptionKey) if existingDeviceCreds != "" { credentials = &glalby.GreenlightCredentials{ GlCreds: existingDeviceCreds, } - svc.Logger.Info("Using saved greenlight credentials") + logger.Info("Using saved greenlight credentials") } if credentials == nil { - svc.Logger.Info("No greenlight credentials found, attempting to recover existing node") + logger.Info("No greenlight credentials found, attempting to recover existing node") recoveredCredentials, err := glalby.Recover(mnemonic) credentials = &recoveredCredentials if err != nil { - svc.Logger.Errorf("Failed to recover node: %v", err) - svc.Logger.Infof("Trying to register instead...") + logger.Errorf("Failed to recover node: %v", err) + logger.Infof("Trying to register instead...") recoveredCredentials, err := glalby.Register(mnemonic, inviteCode) credentials = &recoveredCredentials if err != nil { - svc.Logger.Fatalf("Failed to register new node") + logger.Fatalf("Failed to register new node") } } if credentials == nil || credentials.GlCreds == "" { return nil, errors.New("unexpected response from Recover") } - svc.cfg.SetUpdate(DEVICE_CREDENTIALS_KEY, credentials.GlCreds, encryptionKey) + cfg.SetUpdate(DEVICE_CREDENTIALS_KEY, credentials.GlCreds, encryptionKey) } client, err := glalby.NewBlockingGreenlightAlbyClient(mnemonic, *credentials) @@ -88,7 +90,7 @@ func NewGreenlightService(svc *Service, mnemonic, inviteCode, workDir, encryptio gs := GreenlightService{ workdir: newpath, client: client, - svc: svc, + logger: logger, } nodeInfo, err := client.GetInfo() @@ -105,23 +107,23 @@ func NewGreenlightService(svc *Service, mnemonic, inviteCode, workDir, encryptio func (gs *GreenlightService) Shutdown() error { _, err := gs.client.Shutdown() if err != nil { - gs.svc.Logger.WithError(err).Error("Failed to shutdown greenlight node") + gs.logger.WithError(err).Error("Failed to shutdown greenlight node") return err } return nil } -func (gs *GreenlightService) SendPaymentSync(ctx context.Context, payReq string) (*lnclient.Nip47PayInvoiceResponse, error) { +func (gs *GreenlightService) SendPaymentSync(ctx context.Context, payReq string) (*lnclient.PayInvoiceResponse, error) { response, err := gs.client.Pay(glalby.PayRequest{ Bolt11: payReq, }) if err != nil { - gs.svc.Logger.Errorf("Failed to send payment: %v", err) + gs.logger.Errorf("Failed to send payment: %v", err) return nil, err } log.Printf("SendPaymentSync succeeded: %v", response.Preimage) - return &lnclient.Nip47PayInvoiceResponse{ + return &lnclient.PayInvoiceResponse{ Preimage: response.Preimage, }, nil } @@ -146,7 +148,7 @@ func (gs *GreenlightService) SendKeysend(ctx context.Context, amount int64, dest }) if err != nil { - gs.svc.Logger.Errorf("Failed to send keysend payment: %v", err) + gs.logger.Errorf("Failed to send keysend payment: %v", err) return "", err } @@ -157,7 +159,7 @@ func (gs *GreenlightService) GetBalance(ctx context.Context) (balance int64, err response, err := gs.client.ListFunds(glalby.ListFundsRequest{}) if err != nil { - gs.svc.Logger.Errorf("Failed to list funds: %v", err) + gs.logger.Errorf("Failed to list funds: %v", err) return 0, err } @@ -171,7 +173,7 @@ func (gs *GreenlightService) GetBalance(ctx context.Context) (balance int64, err return balance, nil } -func (gs *GreenlightService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *Nip47Transaction, err error) { +func (gs *GreenlightService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *nip47.Transaction, err error) { uexpiry := uint64(expiry) // TODO: it seems description hash cannot be passed to greenlight invoice, err := gs.client.MakeInvoice(glalby.MakeInvoiceRequest{ @@ -182,13 +184,13 @@ func (gs *GreenlightService) MakeInvoice(ctx context.Context, amount int64, desc }) if err != nil { - gs.svc.Logger.Errorf("MakeInvoice failed: %v", err) + gs.logger.Errorf("MakeInvoice failed: %v", err) return nil, err } paymentRequest, err := decodepay.Decodepay(strings.ToLower(invoice.Bolt11)) if err != nil { - gs.svc.Logger.WithFields(logrus.Fields{ + gs.logger.WithFields(logrus.Fields{ "invoice": invoice.Bolt11, }).Errorf("Failed to decode bolt11 invoice: %v", invoice.Bolt11) return nil, err @@ -197,7 +199,7 @@ func (gs *GreenlightService) MakeInvoice(ctx context.Context, amount int64, desc description = paymentRequest.Description descriptionHash = paymentRequest.DescriptionHash expiresAt := int64(invoice.ExpiresAt) - transaction = &Nip47Transaction{ + transaction = &nip47.Transaction{ Type: "incoming", Invoice: invoice.Bolt11, Description: description, @@ -211,13 +213,13 @@ func (gs *GreenlightService) MakeInvoice(ctx context.Context, amount int64, desc return transaction, nil } -func (gs *GreenlightService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *Nip47Transaction, err error) { +func (gs *GreenlightService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *nip47.Transaction, err error) { response, err := gs.client.ListInvoices(glalby.ListInvoicesRequest{ PaymentHash: &paymentHash, }) if err != nil { - gs.svc.Logger.Errorf("ListInvoices failed: %v", err) + gs.logger.Errorf("ListInvoices failed: %v", err) return nil, err } @@ -233,22 +235,22 @@ func (gs *GreenlightService) LookupInvoice(ctx context.Context, paymentHash stri transaction, err = gs.greenlightInvoiceToTransaction(&invoice) if err != nil { - gs.svc.Logger.Errorf("Failed to map invoice: %v", err) + gs.logger.Errorf("Failed to map invoice: %v", err) return nil, err } return transaction, nil } -func (gs *GreenlightService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []Nip47Transaction, err error) { +func (gs *GreenlightService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []nip47.Transaction, err error) { listInvoicesResponse, err := gs.client.ListInvoices(glalby.ListInvoicesRequest{}) if err != nil { - gs.svc.Logger.Errorf("ListInvoices failed: %v", err) + gs.logger.Errorf("ListInvoices failed: %v", err) return nil, err } - transactions = []Nip47Transaction{} + transactions = []nip47.Transaction{} if err != nil { log.Printf("ListInvoices failed: %v", err) @@ -272,7 +274,7 @@ func (gs *GreenlightService) ListTransactions(ctx context.Context, from, until, listPaymentsResponse, err := gs.client.ListPayments(glalby.ListPaymentsRequest{}) if err != nil { - gs.svc.Logger.Errorf("ListPayments failed: %v", err) + gs.logger.Errorf("ListPayments failed: %v", err) return nil, err } @@ -341,7 +343,7 @@ func (gs *GreenlightService) GetInfo(ctx context.Context) (info *lnclient.NodeIn nodeInfo, err := gs.client.GetInfo() if err != nil { - gs.svc.Logger.Errorf("GetInfo failed: %v", err) + gs.logger.Errorf("GetInfo failed: %v", err) return nil, err } @@ -359,7 +361,7 @@ func (gs *GreenlightService) ListChannels(ctx context.Context) ([]lnclient.Chann response, err := gs.client.ListFunds(glalby.ListFundsRequest{}) if err != nil { - gs.svc.Logger.Errorf("Failed to list funds: %v", err) + gs.logger.Errorf("Failed to list funds: %v", err) return nil, err } @@ -396,7 +398,7 @@ func (gs *GreenlightService) ListChannels(ctx context.Context) ([]lnclient.Chann func (gs *GreenlightService) GetNodeConnectionInfo(ctx context.Context) (nodeConnectionInfo *lnclient.NodeConnectionInfo, err error) { info, err := gs.GetInfo(ctx) if err != nil { - gs.svc.Logger.Errorf("GetInfo failed: %v", err) + gs.logger.Errorf("GetInfo failed: %v", err) return nil, err } return &lnclient.NodeConnectionInfo{ @@ -419,7 +421,7 @@ func (gs *GreenlightService) ConnectPeer(ctx context.Context, connectPeerRequest Port: port, }) if err != nil { - gs.svc.Logger.Errorf("ConnectPeer failed: %v", err) + gs.logger.Errorf("ConnectPeer failed: %v", err) return err } return nil @@ -436,7 +438,7 @@ func (gs *GreenlightService) OpenChannel(ctx context.Context, openChannelRequest // Minconf: &minConf, }) if err != nil { - gs.svc.Logger.Errorf("OpenChannel failed: %v", err) + gs.logger.Errorf("OpenChannel failed: %v", err) return nil, err } @@ -450,7 +452,7 @@ func (gs *GreenlightService) CloseChannel(ctx context.Context, closeChannelReque Id: closeChannelRequest.ChannelId, }) if err != nil { - gs.svc.Logger.WithError(err).Error("CloseChannel failed") + gs.logger.WithError(err).Error("CloseChannel failed") return nil, err } @@ -461,7 +463,7 @@ func (gs *GreenlightService) GetNewOnchainAddress(ctx context.Context) (string, newAddressResponse, err := gs.client.NewAddress(glalby.NewAddressRequest{}) if err != nil { - gs.svc.Logger.Errorf("NewAddress failed: %v", err) + gs.logger.Errorf("NewAddress failed: %v", err) return "", err } if newAddressResponse.Bech32 == nil { @@ -473,10 +475,10 @@ func (gs *GreenlightService) GetNewOnchainAddress(ctx context.Context) (string, func (gs *GreenlightService) GetOnchainBalance(ctx context.Context) (*lnclient.OnchainBalanceResponse, error) { response, err := gs.client.ListFunds(glalby.ListFundsRequest{}) - gs.svc.Logger.WithField("response", response).Info("Listed funds") + gs.logger.WithField("response", response).Info("Listed funds") if err != nil { - gs.svc.Logger.Errorf("Failed to list funds: %v", err) + gs.logger.Errorf("Failed to list funds: %v", err) return nil, err } @@ -505,10 +507,10 @@ func (gs *GreenlightService) RedeemOnchainFunds(ctx context.Context, toAddress s Amount: &amountAll, }) if err != nil { - gs.svc.Logger.WithError(err).Error("Withdraw failed") + gs.logger.WithError(err).Error("Withdraw failed") return "", err } - gs.svc.Logger.WithField("txId", txId).Info("Redeeming On-Chain funds") + gs.logger.WithField("txId", txId).Info("Redeeming On-Chain funds") return txId.Txid, nil } @@ -535,14 +537,14 @@ func (gs *GreenlightService) SignMessage(ctx context.Context, message string) (s }) if err != nil { - gs.svc.Logger.Errorf("SignMessage failed: %v", err) + gs.logger.Errorf("SignMessage failed: %v", err) return "", err } return response.Zbase, nil } -func (gs *GreenlightService) greenlightInvoiceToTransaction(invoice *glalby.ListInvoicesInvoice) (*Nip47Transaction, error) { +func (gs *GreenlightService) greenlightInvoiceToTransaction(invoice *glalby.ListInvoicesInvoice) (*nip47.Transaction, error) { description := "" descriptionHash := "" if invoice.Description != nil { @@ -551,7 +553,7 @@ func (gs *GreenlightService) greenlightInvoiceToTransaction(invoice *glalby.List bolt11 := *invoice.Bolt11 paymentRequest, err := decodepay.Decodepay(strings.ToLower(bolt11)) if err != nil { - gs.svc.Logger.WithFields(logrus.Fields{ + gs.logger.WithFields(logrus.Fields{ "invoice": bolt11, }).Errorf("Failed to decode bolt11 invoice: %v", bolt11) return nil, err @@ -580,7 +582,7 @@ func (gs *GreenlightService) greenlightInvoiceToTransaction(invoice *glalby.List settledAt = &paidAt } - transaction := &Nip47Transaction{ + transaction := &nip47.Transaction{ Type: "incoming", Invoice: bolt11, Description: description, @@ -603,14 +605,14 @@ func (gs *GreenlightService) ResetRouter(key string) error { func (gs *GreenlightService) GetBalances(ctx context.Context) (*lnclient.BalancesResponse, error) { onchainBalance, err := gs.GetOnchainBalance(ctx) if err != nil { - gs.svc.Logger.WithError(err).Error("Failed to retrieve onchain balance") + gs.logger.WithError(err).Error("Failed to retrieve onchain balance") return nil, err } response, err := gs.client.ListFunds(glalby.ListFundsRequest{}) if err != nil { - gs.svc.Logger.Errorf("Failed to list funds: %v", err) + gs.logger.Errorf("Failed to list funds: %v", err) return nil, err } @@ -662,3 +664,5 @@ func (gs *GreenlightService) GetNodeStatus(ctx context.Context) (nodeStatus *lnc func (gs *GreenlightService) GetNetworkGraph(nodeIds []string) (lnclient.NetworkGraphResponse, error) { return nil, nil } + +func (gs *GreenlightService) UpdateLastWalletSyncRequest() {} diff --git a/models/greenlight/greenlight.go b/lnclient/greenlight/models.go similarity index 97% rename from models/greenlight/greenlight.go rename to lnclient/greenlight/models.go index 6697c029..4e9fddce 100644 --- a/models/greenlight/greenlight.go +++ b/lnclient/greenlight/models.go @@ -1,7 +1,6 @@ -// TODO: move to greenlight/models.go package greenlight -import "github.com/getAlby/nostr-wallet-connect/models/lnclient" +import "github.com/getAlby/nostr-wallet-connect/lnclient" type NodeInfo struct { ID string `json:"id"` diff --git a/ldk.go b/lnclient/ldk/ldk.go similarity index 74% rename from ldk.go rename to lnclient/ldk/ldk.go index f55fef49..9a92e45f 100644 --- a/ldk.go +++ b/lnclient/ldk/ldk.go @@ -1,4 +1,4 @@ -package main +package ldk import ( "context" @@ -20,27 +20,31 @@ import ( decodepay "github.com/nbd-wtf/ln-decodepay" "github.com/sirupsen/logrus" + "github.com/getAlby/nostr-wallet-connect/config" "github.com/getAlby/nostr-wallet-connect/events" - "github.com/getAlby/nostr-wallet-connect/models/config" - "github.com/getAlby/nostr-wallet-connect/models/lnclient" - "github.com/getAlby/nostr-wallet-connect/models/lsp" + "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/lsp" + "github.com/getAlby/nostr-wallet-connect/nip47" + "github.com/getAlby/nostr-wallet-connect/utils" ) type LDKService struct { - svc *Service - workdir string - node *ldk_node.Node - ldkEventBroadcaster LDKEventBroadcaster - cancel context.CancelFunc - network string - eventPublisher events.EventPublisher - syncing bool - lastSync time.Time + workdir string + node *ldk_node.Node + ldkEventBroadcaster LDKEventBroadcaster + cancel context.CancelFunc + network string + eventPublisher events.EventPublisher + syncing bool + lastSync time.Time + logger *logrus.Logger + cfg config.Config + lastWalletSyncRequest time.Time } const resetRouterKey = "ResetRouter" -func NewLDKService(ctx context.Context, svc *Service, mnemonic, workDir string, network string, esploraServer string, gossipSource string) (result lnclient.LNClient, err error) { +func NewLDKService(ctx context.Context, logger *logrus.Logger, cfg config.Config, eventPublisher events.EventPublisher, mnemonic, workDir string, network string, esploraServer string, gossipSource string) (result lnclient.LNClient, err error) { if mnemonic == "" || workDir == "" { return nil, errors.New("one or more required LDK configuration are missing") } @@ -79,7 +83,7 @@ func NewLDKService(ctx context.Context, svc *Service, mnemonic, workDir string, config.ListeningAddresses = &listeningAddresses config.LogDirPath = &logDirPath - logLevel, err := strconv.Atoi(svc.cfg.Env.LDKLogLevel) + logLevel, err := strconv.Atoi(cfg.GetEnv().LDKLogLevel) if err == nil { config.LogLevel = ldk_node.LogLevel(logLevel) } @@ -88,10 +92,10 @@ func NewLDKService(ctx context.Context, svc *Service, mnemonic, workDir string, builder.SetNetwork(network) builder.SetEsploraServer(esploraServer) if gossipSource != "" { - svc.Logger.WithField("gossipSource", gossipSource).Warn("LDK RGS instance set") + logger.WithField("gossipSource", gossipSource).Warn("LDK RGS instance set") builder.SetGossipSourceRgs(gossipSource) } else { - svc.Logger.Warn("No LDK RGS instance set") + logger.Warn("No LDK RGS instance set") } builder.SetStorageDirPath(filepath.Join(newpath, "./storage")) @@ -103,33 +107,34 @@ func NewLDKService(ctx context.Context, svc *Service, mnemonic, workDir string, node, err := builder.Build() if err != nil { - svc.Logger.Errorf("Failed to create LDK node: %v", err) + logger.Errorf("Failed to create LDK node: %v", err) return nil, err } ldkEventConsumer := make(chan *ldk_node.Event) ldkCtx, cancel := context.WithCancel(ctx) - ldkEventBroadcaster := NewLDKEventBroadcaster(svc.Logger, ldkCtx, ldkEventConsumer) + ldkEventBroadcaster := NewLDKEventBroadcaster(logger, ldkCtx, ldkEventConsumer) ls := LDKService{ workdir: newpath, node: node, - svc: svc, cancel: cancel, ldkEventBroadcaster: ldkEventBroadcaster, network: network, - eventPublisher: svc.EventPublisher, + eventPublisher: eventPublisher, + logger: logger, + cfg: cfg, } // TODO: remove when LDK supports this - deleteOldLDKLogs(svc.Logger, logDirPath) + deleteOldLDKLogs(logger, logDirPath) go func() { // delete old LDK logs every 24 hours ticker := time.NewTicker(24 * time.Hour) for { select { case <-ticker.C: - deleteOldLDKLogs(svc.Logger, logDirPath) + deleteOldLDKLogs(logger, logDirPath) case <-ldkCtx.Done(): return } @@ -152,7 +157,7 @@ func NewLDKService(ctx context.Context, svc *Service, mnemonic, workDir string, continue } - ls.logLdkEvent(ldkCtx, event) + ls.handleLdkEvent(ldkCtx, event) ldkEventConsumer <- event node.EventHandled() @@ -162,12 +167,12 @@ func NewLDKService(ctx context.Context, svc *Service, mnemonic, workDir string, err = node.Start() if err != nil { - svc.Logger.Errorf("Failed to start LDK node: %v", err) + logger.Errorf("Failed to start LDK node: %v", err) return nil, err } nodeId := node.NodeId() - svc.Logger.WithFields(logrus.Fields{ + logger.WithFields(logrus.Fields{ "nodeId": nodeId, "status": node.Status(), }).Info("Started LDK node. Syncing wallet...") @@ -175,17 +180,17 @@ func NewLDKService(ctx context.Context, svc *Service, mnemonic, workDir string, syncStartTime := time.Now() err = node.SyncWallets() if err != nil { - svc.Logger.WithError(err).Error("Failed to sync LDK wallets") + logger.WithError(err).Error("Failed to sync LDK wallets") shutdownErr := ls.Shutdown() if shutdownErr != nil { - svc.Logger.WithError(shutdownErr).Error("Failed to shutdown LDK node") + logger.WithError(shutdownErr).Error("Failed to shutdown LDK node") } return nil, err } ls.lastSync = time.Now() - svc.Logger.WithFields(logrus.Fields{ + logger.WithFields(logrus.Fields{ "nodeId": nodeId, "status": node.Status(), "duration": math.Ceil(time.Since(syncStartTime).Seconds()), @@ -193,18 +198,19 @@ func NewLDKService(ctx context.Context, svc *Service, mnemonic, workDir string, if ls.network == "bitcoin" { // try to connect to some peers to retrieve P2P gossip data. TODO: Remove once LDK can correctly do gossip with CLN and Eclair nodes + // see https://github.com/lightningdevkit/rust-lightning/issues/3075 peers := []string{ "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581@45.79.192.236:9735", // Olympus "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1@192.243.215.102:9735", // LQwD "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226@170.75.163.209:9735", // WoS "02fcc5bfc48e83f06c04483a2985e1c390cb0f35058baa875ad2053858b8e80dbd@35.239.148.251:9735", // Blink } - svc.Logger.Info("Connecting to some peers to retrieve P2P gossip data") + logger.Info("Connecting to some peers to retrieve P2P gossip data") for _, peer := range peers { parts := strings.FieldsFunc(peer, func(r rune) bool { return r == '@' || r == ':' }) port, err := strconv.ParseUint(parts[2], 10, 16) if err != nil { - svc.Logger.WithError(err).Error("Failed to parse port number") + logger.WithError(err).Error("Failed to parse port number") continue } err = ls.ConnectPeer(ctx, &lnclient.ConnectPeerRequest{ @@ -213,7 +219,7 @@ func NewLDKService(ctx context.Context, svc *Service, mnemonic, workDir string, Port: uint16(port), }) if err != nil { - svc.Logger.WithField("peer", peer).WithError(err).Error("Failed to connect to peer") + logger.WithField("peer", peer).WithError(err).Error("Failed to connect to peer") } } } @@ -228,26 +234,26 @@ func NewLDKService(ctx context.Context, svc *Service, mnemonic, workDir string, return case <-time.After(MIN_SYNC_INTERVAL): - if time.Since(ls.svc.lastWalletSyncRequest) > MIN_SYNC_INTERVAL && time.Since(ls.lastSync) < MAX_SYNC_INTERVAL { - // ls.svc.Logger.Debug("skipping background wallet sync") + if time.Since(ls.lastWalletSyncRequest) > MIN_SYNC_INTERVAL && time.Since(ls.lastSync) < MAX_SYNC_INTERVAL { + // ls.logger.Debug("skipping background wallet sync") continue } - ls.svc.Logger.Info("Starting background wallet sync") + ls.logger.Info("Starting background wallet sync") syncStartTime := time.Now() ls.syncing = true err = node.SyncWallets() ls.syncing = false if err != nil { - svc.Logger.WithError(err).Error("Failed to sync LDK wallets") + logger.WithError(err).Error("Failed to sync LDK wallets") // try again at next MIN_SYNC_INTERVAL continue } ls.lastSync = time.Now() - svc.Logger.WithFields(logrus.Fields{ + logger.WithFields(logrus.Fields{ "nodeId": nodeId, "status": node.Status(), "duration": math.Ceil(time.Since(syncStartTime).Seconds()), @@ -261,23 +267,23 @@ func NewLDKService(ctx context.Context, svc *Service, mnemonic, workDir string, func (ls *LDKService) Shutdown() error { if ls.node == nil { - ls.svc.Logger.Infof("LDK client already shut down") + ls.logger.Infof("LDK client already shut down") return nil } // make sure nothing else can use it node := ls.node ls.node = nil - ls.svc.Logger.Infof("shutting down LDK client") - ls.svc.Logger.Infof("cancelling LDK context") + ls.logger.Infof("shutting down LDK client") + ls.logger.Infof("cancelling LDK context") ls.cancel() for ls.syncing { - ls.svc.Logger.Infof("Waiting for background sync to finish before stopping LDK node...") + ls.logger.Infof("Waiting for background sync to finish before stopping LDK node...") time.Sleep(1 * time.Second) } - ls.svc.Logger.Infof("stopping LDK node") + ls.logger.Infof("stopping LDK node") shutdownChannel := make(chan error) go func() { shutdownChannel <- node.Stop() @@ -286,44 +292,44 @@ func (ls *LDKService) Shutdown() error { select { case err := <-shutdownChannel: if err != nil { - ls.svc.Logger.WithError(err).Error("Failed to stop LDK node") + ls.logger.WithError(err).Error("Failed to stop LDK node") // do not return error - we still need to destroy the node } else { - ls.svc.Logger.Info("LDK stop node succeeded") + ls.logger.Info("LDK stop node succeeded") } case <-time.After(120 * time.Second): - ls.svc.Logger.Error("Timeout shutting down LDK node after 120 seconds") + ls.logger.Error("Timeout shutting down LDK node after 120 seconds") } - ls.svc.Logger.Infof("Destroying node object") + ls.logger.Infof("Destroying node object") node.Destroy() ls.resetRouterInternal() - ls.svc.Logger.Infof("LDK shutdown complete") + ls.logger.Infof("LDK shutdown complete") return nil } func (ls *LDKService) resetRouterInternal() { - key, err := ls.svc.cfg.Get(resetRouterKey, "") + key, err := ls.cfg.Get(resetRouterKey, "") if err != nil { - ls.svc.Logger.Error("Failed to retrieve ResetRouter key") + ls.logger.Error("Failed to retrieve ResetRouter key") } if key != "" { - ls.svc.cfg.SetUpdate(resetRouterKey, "", "") - ls.svc.Logger.WithField("key", key).Info("Resetting router") + ls.cfg.SetUpdate(resetRouterKey, "", "") + ls.logger.WithField("key", key).Info("Resetting router") ldkDbPath := filepath.Join(ls.workdir, "storage", "ldk_node_data.sqlite") if _, err := os.Stat(ldkDbPath); errors.Is(err, os.ErrNotExist) { - ls.svc.Logger.Error("Could not find LDK database") + ls.logger.Error("Could not find LDK database") return } ldkDb, err := sql.Open("sqlite", ldkDbPath) if err != nil { - ls.svc.Logger.Error("Could not open LDK DB file") + ls.logger.Error("Could not open LDK DB file") return } @@ -339,43 +345,43 @@ func (ls *LDKService) resetRouterInternal() { case "NetworkGraph": command = "delete from ldk_node_data where key = 'network_graph';VACUUM;" default: - ls.svc.Logger.WithField("key", key).Error("Unknown reset router key") + ls.logger.WithField("key", key).Error("Unknown reset router key") return } result, err := ldkDb.Exec(command) if err != nil { - ls.svc.Logger.WithError(err).Error("Failed execute reset command") + ls.logger.WithError(err).Error("Failed execute reset command") return } rowsAffected, err := result.RowsAffected() if err != nil { - ls.svc.Logger.WithError(err).Error("Failed to get rows affected") + ls.logger.WithError(err).Error("Failed to get rows affected") return } - ls.svc.Logger.WithFields(logrus.Fields{ + ls.logger.WithFields(logrus.Fields{ "rowsAffected": rowsAffected, }).Info("Reset router") if err != nil { - ls.svc.Logger.WithField("key", key).WithError(err).Error("ResetRouter failed") + ls.logger.WithField("key", key).WithError(err).Error("ResetRouter failed") } } } -func (gs *LDKService) SendPaymentSync(ctx context.Context, invoice string) (*lnclient.Nip47PayInvoiceResponse, error) { +func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string) (*lnclient.PayInvoiceResponse, error) { paymentRequest, err := decodepay.Decodepay(invoice) if err != nil { - gs.svc.Logger.WithFields(logrus.Fields{ + ls.logger.WithFields(logrus.Fields{ "bolt11": invoice, }).Errorf("Failed to decode bolt11 invoice: %v", err) return nil, err } - maxSpendable := gs.getMaxSpendable() + maxSpendable := ls.getMaxSpendable() if paymentRequest.MSatoshi > maxSpendable { - gs.eventPublisher.Publish(&events.Event{ + ls.eventPublisher.Publish(&events.Event{ Event: "nwc_outgoing_liquidity_required", Properties: map[string]interface{}{ //"amount": amount / 1000, @@ -387,12 +393,12 @@ func (gs *LDKService) SendPaymentSync(ctx context.Context, invoice string) (*lnc } paymentStart := time.Now() - ldkEventSubscription := gs.ldkEventBroadcaster.Subscribe() - defer gs.ldkEventBroadcaster.CancelSubscription(ldkEventSubscription) + ldkEventSubscription := ls.ldkEventBroadcaster.Subscribe() + defer ls.ldkEventBroadcaster.CancelSubscription(ldkEventSubscription) - paymentHash, err := gs.node.Bolt11Payment().Send(invoice) + paymentHash, err := ls.node.Bolt11Payment().Send(invoice) if err != nil { - gs.svc.Logger.WithError(err).Error("SendPayment failed") + ls.logger.WithError(err).Error("SendPayment failed") return nil, err } fee := uint64(0) @@ -405,23 +411,23 @@ func (gs *LDKService) SendPaymentSync(ctx context.Context, invoice string) (*lnc eventPaymentFailed, isEventPaymentFailedEvent := (*event).(ldk_node.EventPaymentFailed) if isEventPaymentSuccessfulEvent && eventPaymentSuccessful.PaymentHash == paymentHash { - gs.svc.Logger.Info("Got payment success event") - payment := gs.node.Payment(paymentHash) + ls.logger.Info("Got payment success event") + payment := ls.node.Payment(paymentHash) if payment == nil { - gs.svc.Logger.Errorf("Couldn't find payment by payment hash: %v", paymentHash) + ls.logger.Errorf("Couldn't find payment by payment hash: %v", paymentHash) return nil, errors.New("Payment not found") } bolt11PaymentKind, ok := payment.Kind.(ldk_node.PaymentKindBolt11) if !ok { - gs.svc.Logger.WithFields(logrus.Fields{ + ls.logger.WithFields(logrus.Fields{ "payment": payment, }).Error("Payment is not a bolt11 kind") } if bolt11PaymentKind.Preimage == nil { - gs.svc.Logger.Errorf("No payment preimage for payment hash: %v", paymentHash) + ls.logger.Errorf("No payment preimage for payment hash: %v", paymentHash) return nil, errors.New("Payment preimage not found") } preimage = *bolt11PaymentKind.Preimage @@ -454,7 +460,7 @@ func (gs *LDKService) SendPaymentSync(ctx context.Context, invoice string) (*lnc failureReasonMessage = "UnknownError" } - gs.svc.Logger.WithFields(logrus.Fields{ + ls.logger.WithFields(logrus.Fields{ "paymentHash": paymentHash, "failureReason": failureReason, "failureReasonMessage": failureReasonMessage, @@ -468,18 +474,18 @@ func (gs *LDKService) SendPaymentSync(ctx context.Context, invoice string) (*lnc return nil, errors.New("Payment timed out") } - gs.svc.Logger.WithFields(logrus.Fields{ + ls.logger.WithFields(logrus.Fields{ "duration": time.Since(paymentStart).Milliseconds(), "fee": fee, }).Info("Successful payment") - return &lnclient.Nip47PayInvoiceResponse{ + return &lnclient.PayInvoiceResponse{ Preimage: preimage, Fee: &fee, }, nil } -func (gs *LDKService) SendKeysend(ctx context.Context, amount int64, destination, preimage string, custom_records []lnclient.TLVRecord) (preImage string, err error) { +func (ls *LDKService) SendKeysend(ctx context.Context, amount int64, destination, preimage string, custom_records []lnclient.TLVRecord) (preImage string, err error) { paymentStart := time.Now() customTlvs := []ldk_node.TlvEntry{} @@ -490,16 +496,16 @@ func (gs *LDKService) SendKeysend(ctx context.Context, amount int64, destination }) } - ldkEventSubscription := gs.ldkEventBroadcaster.Subscribe() - defer gs.ldkEventBroadcaster.CancelSubscription(ldkEventSubscription) + ldkEventSubscription := ls.ldkEventBroadcaster.Subscribe() + defer ls.ldkEventBroadcaster.CancelSubscription(ldkEventSubscription) - paymentHash, err := gs.node.SpontaneousPayment().Send(uint64(amount), destination, customTlvs) + paymentHash, err := ls.node.SpontaneousPayment().Send(uint64(amount), destination, customTlvs) if err != nil { - gs.svc.Logger.WithError(err).Error("Keysend failed") + ls.logger.WithError(err).Error("Keysend failed") return "", err } - gs.svc.Logger.Infof("TODO: listen for events %v", paymentHash) + ls.logger.Infof("TODO: listen for events %v", paymentHash) fee := uint64(0) @@ -510,23 +516,23 @@ func (gs *LDKService) SendKeysend(ctx context.Context, amount int64, destination eventPaymentFailed, isEventPaymentFailedEvent := (*event).(ldk_node.EventPaymentFailed) if isEventPaymentSuccessfulEvent && eventPaymentSuccessful.PaymentHash == paymentHash { - gs.svc.Logger.Info("Got payment success event") - payment := gs.node.Payment(paymentHash) + ls.logger.Info("Got payment success event") + payment := ls.node.Payment(paymentHash) if payment == nil { - gs.svc.Logger.Errorf("Couldn't find payment by payment hash: %v", paymentHash) + ls.logger.Errorf("Couldn't find payment by payment hash: %v", paymentHash) return "", errors.New("Payment not found") } spontaneousPaymentKind, ok := payment.Kind.(ldk_node.PaymentKindSpontaneous) if !ok { - gs.svc.Logger.WithFields(logrus.Fields{ + ls.logger.WithFields(logrus.Fields{ "payment": payment, }).Error("Payment is not a spontaneous kind") } if spontaneousPaymentKind.Preimage == nil { - gs.svc.Logger.Errorf("No payment preimage for payment hash: %v", paymentHash) + ls.logger.Errorf("No payment preimage for payment hash: %v", paymentHash) return "", errors.New("Payment preimage not found") } preimage = *spontaneousPaymentKind.Preimage @@ -559,7 +565,7 @@ func (gs *LDKService) SendKeysend(ctx context.Context, amount int64, destination failureReasonMessage = "UnknownError" } - gs.svc.Logger.WithFields(logrus.Fields{ + ls.logger.WithFields(logrus.Fields{ "paymentHash": paymentHash, "failureReason": failureReason, "failureReasonMessage": failureReasonMessage, @@ -573,15 +579,15 @@ func (gs *LDKService) SendKeysend(ctx context.Context, amount int64, destination return "", errors.New("keysend payment timed out") } - gs.svc.Logger.WithFields(logrus.Fields{ + ls.logger.WithFields(logrus.Fields{ "duration": time.Since(paymentStart).Milliseconds(), "fee": fee, }).Info("Successful keysend payment") return preimage, nil } -func (gs *LDKService) GetBalance(ctx context.Context) (balance int64, err error) { - channels := gs.node.ListChannels() +func (ls *LDKService) GetBalance(ctx context.Context) (balance int64, err error) { + channels := ls.node.ListChannels() balance = 0 for _, channel := range channels { @@ -593,9 +599,9 @@ func (gs *LDKService) GetBalance(ctx context.Context) (balance int64, err error) return balance, nil } -func (gs *LDKService) getMaxReceivable() int64 { +func (ls *LDKService) getMaxReceivable() int64 { var receivable int64 = 0 - channels := gs.node.ListChannels() + channels := ls.node.ListChannels() for _, channel := range channels { if channel.IsUsable { receivable += min(int64(channel.InboundCapacityMsat), int64(*channel.InboundHtlcMaximumMsat)) @@ -604,9 +610,9 @@ func (gs *LDKService) getMaxReceivable() int64 { return int64(receivable) } -func (gs *LDKService) getMaxSpendable() int64 { +func (ls *LDKService) getMaxSpendable() int64 { var spendable int64 = 0 - channels := gs.node.ListChannels() + channels := ls.node.ListChannels() for _, channel := range channels { if channel.IsUsable { spendable += min(int64(channel.OutboundCapacityMsat), int64(*channel.CounterpartyOutboundHtlcMaximumMsat)) @@ -615,12 +621,12 @@ func (gs *LDKService) getMaxSpendable() int64 { return int64(spendable) } -func (gs *LDKService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *Nip47Transaction, err error) { +func (ls *LDKService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *nip47.Transaction, err error) { - maxReceivable := gs.getMaxReceivable() + maxReceivable := ls.getMaxReceivable() if amount > maxReceivable { - gs.eventPublisher.Publish(&events.Event{ + ls.eventPublisher.Publish(&events.Event{ Event: "nwc_incoming_liquidity_required", Properties: map[string]interface{}{ //"amount": amount / 1000, @@ -632,19 +638,19 @@ func (gs *LDKService) MakeInvoice(ctx context.Context, amount int64, description } // TODO: support passing description hash - invoice, err := gs.node.Bolt11Payment().Receive(uint64(amount), + invoice, err := ls.node.Bolt11Payment().Receive(uint64(amount), description, uint32(expiry)) if err != nil { - gs.svc.Logger.WithError(err).Error("MakeInvoice failed") + ls.logger.WithError(err).Error("MakeInvoice failed") return nil, err } var expiresAt *int64 paymentRequest, err := decodepay.Decodepay(invoice) if err != nil { - gs.svc.Logger.WithFields(logrus.Fields{ + ls.logger.WithFields(logrus.Fields{ "bolt11": invoice, }).Errorf("Failed to decode bolt11 invoice: %v", err) @@ -655,7 +661,7 @@ func (gs *LDKService) MakeInvoice(ctx context.Context, amount int64, description description = paymentRequest.Description descriptionHash = paymentRequest.DescriptionHash - transaction = &Nip47Transaction{ + transaction = &nip47.Transaction{ Type: "incoming", Invoice: invoice, PaymentHash: paymentRequest.PaymentHash, @@ -669,26 +675,26 @@ func (gs *LDKService) MakeInvoice(ctx context.Context, amount int64, description return transaction, nil } -func (gs *LDKService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *Nip47Transaction, err error) { +func (ls *LDKService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *nip47.Transaction, err error) { - payment := gs.node.Payment(paymentHash) + payment := ls.node.Payment(paymentHash) if payment == nil { - gs.svc.Logger.Errorf("Couldn't find payment by payment hash: %v", paymentHash) + ls.logger.Errorf("Couldn't find payment by payment hash: %v", paymentHash) return nil, errors.New("Payment not found") } - transaction, err = gs.ldkPaymentToTransaction(payment) + transaction, err = ls.ldkPaymentToTransaction(payment) if err != nil { - gs.svc.Logger.Errorf("Failed to map transaction: %v", err) + ls.logger.Errorf("Failed to map transaction: %v", err) return nil, err } return transaction, nil } -func (ls *LDKService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []Nip47Transaction, err error) { - transactions = []Nip47Transaction{} +func (ls *LDKService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []nip47.Transaction, err error) { + transactions = []nip47.Transaction{} // TODO: support pagination payments := ls.node.ListPayments() @@ -698,7 +704,7 @@ func (ls *LDKService) ListTransactions(ctx context.Context, from, until, limit, transaction, err := ls.ldkPaymentToTransaction(&payment) if err != nil { - ls.svc.Logger.Errorf("Failed to map transaction: %v", err) + ls.logger.Errorf("Failed to map transaction: %v", err) continue } @@ -726,7 +732,7 @@ func (ls *LDKService) ListTransactions(ctx context.Context, from, until, limit, if offset < uint64(len(transactions)) { transactions = transactions[offset:] } else { - transactions = []Nip47Transaction{} + transactions = []nip47.Transaction{} } } @@ -734,32 +740,32 @@ func (ls *LDKService) ListTransactions(ctx context.Context, from, until, limit, transactions = transactions[:limit] } - // ls.svc.Logger.WithField("transactions", transactions).Debug("Listed transactions") + // ls.logger.WithField("transactions", transactions).Debug("Listed transactions") return transactions, nil } -func (gs *LDKService) GetInfo(ctx context.Context) (info *lnclient.NodeInfo, err error) { +func (ls *LDKService) GetInfo(ctx context.Context) (info *lnclient.NodeInfo, err error) { // TODO: should alias, color be configured in LDK-node? or can we manage them in NWC? // an alias is only needed if the user has public channels and wants their node to be publicly visible? - status := gs.node.Status() + status := ls.node.Status() return &lnclient.NodeInfo{ Alias: "NWC", Color: "#897FFF", - Pubkey: gs.node.NodeId(), - Network: gs.network, + Pubkey: ls.node.NodeId(), + Network: ls.network, BlockHeight: status.CurrentBestBlock.Height, BlockHash: status.CurrentBestBlock.BlockHash, }, nil } -func (gs *LDKService) ListChannels(ctx context.Context) ([]lnclient.Channel, error) { +func (ls *LDKService) ListChannels(ctx context.Context) ([]lnclient.Channel, error) { - ldkChannels := gs.node.ListChannels() + ldkChannels := ls.node.ListChannels() channels := []lnclient.Channel{} - // gs.svc.Logger.WithFields(logrus.Fields{ + // gs.logger.WithFields(logrus.Fields{ // "channels": ldkChannels, // }).Debug("Listed Channels") @@ -786,7 +792,7 @@ func (gs *LDKService) ListChannels(ctx context.Context) ([]lnclient.Channel, err return channels, nil } -func (gs *LDKService) GetNodeConnectionInfo(ctx context.Context) (nodeConnectionInfo *lnclient.NodeConnectionInfo, err error) { +func (ls *LDKService) GetNodeConnectionInfo(ctx context.Context) (nodeConnectionInfo *lnclient.NodeConnectionInfo, err error) { /*addresses := gs.node.ListeningAddresses() if addresses == nil || len(*addresses) < 1 { return nil, errors.New("no available listening addresses") @@ -798,29 +804,29 @@ func (gs *LDKService) GetNodeConnectionInfo(ctx context.Context) (nodeConnection } port, err := strconv.Atoi(parts[1]) if err != nil { - gs.svc.Logger.WithError(err).Error("ConnectPeer failed") + gs.logger.WithError(err).Error("ConnectPeer failed") return nil, err }*/ return &lnclient.NodeConnectionInfo{ - Pubkey: gs.node.NodeId(), + Pubkey: ls.node.NodeId(), //Address: parts[0], //Port: port, }, nil } -func (gs *LDKService) ConnectPeer(ctx context.Context, connectPeerRequest *lnclient.ConnectPeerRequest) error { - err := gs.node.Connect(connectPeerRequest.Pubkey, connectPeerRequest.Address+":"+strconv.Itoa(int(connectPeerRequest.Port)), true) +func (ls *LDKService) ConnectPeer(ctx context.Context, connectPeerRequest *lnclient.ConnectPeerRequest) error { + err := ls.node.Connect(connectPeerRequest.Pubkey, connectPeerRequest.Address+":"+strconv.Itoa(int(connectPeerRequest.Port)), true) if err != nil { - gs.svc.Logger.WithField("request", connectPeerRequest).WithError(err).Error("ConnectPeer failed") + ls.logger.WithField("request", connectPeerRequest).WithError(err).Error("ConnectPeer failed") return err } return nil } -func (gs *LDKService) OpenChannel(ctx context.Context, openChannelRequest *lnclient.OpenChannelRequest) (*lnclient.OpenChannelResponse, error) { - peers := gs.node.ListPeers() +func (ls *LDKService) OpenChannel(ctx context.Context, openChannelRequest *lnclient.OpenChannelRequest) (*lnclient.OpenChannelResponse, error) { + peers := ls.node.ListPeers() var foundPeer *ldk_node.PeerDetails for _, peer := range peers { if peer.NodeId == openChannelRequest.Pubkey { @@ -834,18 +840,18 @@ func (gs *LDKService) OpenChannel(ctx context.Context, openChannelRequest *lncli return nil, errors.New("node is not peered yet") } - ldkEventSubscription := gs.ldkEventBroadcaster.Subscribe() - defer gs.ldkEventBroadcaster.CancelSubscription(ldkEventSubscription) + ldkEventSubscription := ls.ldkEventBroadcaster.Subscribe() + defer ls.ldkEventBroadcaster.CancelSubscription(ldkEventSubscription) - gs.svc.Logger.Infof("Opening channel with: %v", foundPeer.NodeId) - userChannelId, err := gs.node.ConnectOpenChannel(foundPeer.NodeId, foundPeer.Address, uint64(openChannelRequest.Amount), nil, nil, openChannelRequest.Public) + ls.logger.Infof("Opening channel with: %v", foundPeer.NodeId) + userChannelId, err := ls.node.ConnectOpenChannel(foundPeer.NodeId, foundPeer.Address, uint64(openChannelRequest.Amount), nil, nil, openChannelRequest.Public) if err != nil { - gs.svc.Logger.WithError(err).Error("OpenChannel failed") + ls.logger.WithError(err).Error("OpenChannel failed") return nil, err } // userChannelId allows to locally keep track of the channel (and is also used to close the channel) - gs.svc.Logger.Infof("Funded channel: %v", userChannelId) + ls.logger.Infof("Funded channel: %v", userChannelId) for start := time.Now(); time.Since(start) < time.Second*60; { event := <-ldkEventSubscription @@ -854,7 +860,7 @@ func (gs *LDKService) OpenChannel(ctx context.Context, openChannelRequest *lncli channelClosedEvent, isChannelClosedEvent := (*event).(ldk_node.EventChannelClosed) if isChannelClosedEvent { - gs.svc.Logger.WithFields(logrus.Fields{ + ls.logger.WithFields(logrus.Fields{ "event": channelClosedEvent, }) return nil, fmt.Errorf("failed to open channel: %+v", *channelClosedEvent.Reason) @@ -872,31 +878,31 @@ func (gs *LDKService) OpenChannel(ctx context.Context, openChannelRequest *lncli return nil, errors.New("open channel timeout") } -func (gs *LDKService) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) (*lnclient.CloseChannelResponse, error) { - gs.svc.Logger.WithFields(logrus.Fields{ +func (ls *LDKService) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) (*lnclient.CloseChannelResponse, error) { + ls.logger.WithFields(logrus.Fields{ "request": closeChannelRequest, }).Info("Closing Channel") // TODO: support passing force option - err := gs.node.CloseChannel(closeChannelRequest.ChannelId, closeChannelRequest.NodeId, closeChannelRequest.Force) + err := ls.node.CloseChannel(closeChannelRequest.ChannelId, closeChannelRequest.NodeId, closeChannelRequest.Force) if err != nil { - gs.svc.Logger.WithError(err).Error("CloseChannel failed") + ls.logger.WithError(err).Error("CloseChannel failed") return nil, err } return &lnclient.CloseChannelResponse{}, nil } -func (gs *LDKService) GetNewOnchainAddress(ctx context.Context) (string, error) { - address, err := gs.node.OnchainPayment().NewAddress() +func (ls *LDKService) GetNewOnchainAddress(ctx context.Context) (string, error) { + address, err := ls.node.OnchainPayment().NewAddress() if err != nil { - gs.svc.Logger.WithError(err).Error("NewOnchainAddress failed") + ls.logger.WithError(err).Error("NewOnchainAddress failed") return "", err } return address, nil } -func (gs *LDKService) GetOnchainBalance(ctx context.Context) (*lnclient.OnchainBalanceResponse, error) { - balances := gs.node.ListBalances() - gs.svc.Logger.WithFields(logrus.Fields{ +func (ls *LDKService) GetOnchainBalance(ctx context.Context) (*lnclient.OnchainBalanceResponse, error) { + balances := ls.node.ListBalances() + ls.logger.WithFields(logrus.Fields{ "balances": balances, }).Debug("Listed Balances") return &lnclient.OnchainBalanceResponse{ @@ -906,33 +912,33 @@ func (gs *LDKService) GetOnchainBalance(ctx context.Context) (*lnclient.OnchainB }, nil } -func (gs *LDKService) RedeemOnchainFunds(ctx context.Context, toAddress string) (string, error) { - txId, err := gs.node.OnchainPayment().SendAllToAddress(toAddress) +func (ls *LDKService) RedeemOnchainFunds(ctx context.Context, toAddress string) (string, error) { + txId, err := ls.node.OnchainPayment().SendAllToAddress(toAddress) if err != nil { - gs.svc.Logger.WithError(err).Error("SendAllToOnchainAddress failed") + ls.logger.WithError(err).Error("SendAllToOnchainAddress failed") return "", err } return txId, nil } func (ls *LDKService) ResetRouter(key string) error { - ls.svc.cfg.SetUpdate(resetRouterKey, key, "") + ls.cfg.SetUpdate(resetRouterKey, key, "") return nil } -func (gs *LDKService) SignMessage(ctx context.Context, message string) (string, error) { - sign, err := gs.node.SignMessage([]byte(message)) +func (ls *LDKService) SignMessage(ctx context.Context, message string) (string, error) { + sign, err := ls.node.SignMessage([]byte(message)) if err != nil { - gs.svc.Logger.Errorf("SignMessage failed: %v", err) + ls.logger.Errorf("SignMessage failed: %v", err) return "", err } return sign, nil } -func (gs *LDKService) ldkPaymentToTransaction(payment *ldk_node.PaymentDetails) (*Nip47Transaction, error) { - // gs.svc.Logger.WithField("payment", payment).Debug("Mapping LDK payment to transaction") +func (ls *LDKService) ldkPaymentToTransaction(payment *ldk_node.PaymentDetails) (*nip47.Transaction, error) { + // gs.logger.WithField("payment", payment).Debug("Mapping LDK payment to transaction") transactionType := "incoming" if payment.Direction == ldk_node.PaymentDirectionOutbound { @@ -954,7 +960,7 @@ func (gs *LDKService) ldkPaymentToTransaction(payment *ldk_node.PaymentDetails) bolt11Invoice = *bolt11PaymentKind.Bolt11Invoice paymentRequest, err := decodepay.Decodepay(strings.ToLower(bolt11Invoice)) if err != nil { - gs.svc.Logger.WithFields(logrus.Fields{ + ls.logger.WithFields(logrus.Fields{ "bolt11": bolt11Invoice, }).Errorf("Failed to decode bolt11 invoice: %v", err) @@ -988,7 +994,7 @@ func (gs *LDKService) ldkPaymentToTransaction(payment *ldk_node.PaymentDetails) fee = *payment.FeeMsat } - return &Nip47Transaction{ + return &nip47.Transaction{ Type: transactionType, Preimage: preimage, PaymentHash: paymentHash, @@ -1003,28 +1009,28 @@ func (gs *LDKService) ldkPaymentToTransaction(payment *ldk_node.PaymentDetails) }, nil } -func (gs *LDKService) SendPaymentProbes(ctx context.Context, invoice string) error { - err := gs.node.Bolt11Payment().SendProbes(invoice) +func (ls *LDKService) SendPaymentProbes(ctx context.Context, invoice string) error { + err := ls.node.Bolt11Payment().SendProbes(invoice) if err != nil { - gs.svc.Logger.Errorf("Bolt11Payment.SendProbes failed: %v", err) + ls.logger.Errorf("Bolt11Payment.SendProbes failed: %v", err) return err } return nil } -func (gs *LDKService) SendSpontaneousPaymentProbes(ctx context.Context, amountMsat uint64, nodeId string) error { - err := gs.node.SpontaneousPayment().SendProbes(amountMsat, nodeId) +func (ls *LDKService) SendSpontaneousPaymentProbes(ctx context.Context, amountMsat uint64, nodeId string) error { + err := ls.node.SpontaneousPayment().SendProbes(amountMsat, nodeId) if err != nil { - gs.svc.Logger.Errorf("SpontaneousPayment.SendProbes failed: %v", err) + ls.logger.Errorf("SpontaneousPayment.SendProbes failed: %v", err) return err } return nil } -func (gs *LDKService) ListPeers(ctx context.Context) ([]lnclient.PeerDetails, error) { - peers := gs.node.ListPeers() +func (ls *LDKService) ListPeers(ctx context.Context) ([]lnclient.PeerDetails, error) { + peers := ls.node.ListPeers() ret := make([]lnclient.PeerDetails, 0, len(peers)) for _, peer := range peers { ret = append(ret, lnclient.PeerDetails{ @@ -1070,8 +1076,8 @@ func (ls *LDKService) GetNetworkGraph(nodeIds []string) (lnclient.NetworkGraphRe return networkGraph, nil } -func (gs *LDKService) GetLogOutput(ctx context.Context, maxLen int) ([]byte, error) { - config := gs.node.Config() +func (ls *LDKService) GetLogOutput(ctx context.Context, maxLen int) ([]byte, error) { + config := ls.node.Config() logPath := "" if config.LogDirPath != nil { logPath = *config.LogDirPath @@ -1082,7 +1088,7 @@ func (gs *LDKService) GetLogOutput(ctx context.Context, maxLen int) ([]byte, err allLogFiles, err := filepath.Glob(filepath.Join(logPath, "ldk_node_*.log")) if err != nil { - gs.svc.Logger.WithError(err).Error("GetLogOutput failed to list log files") + ls.logger.WithError(err).Error("GetLogOutput failed to list log files") return nil, err } @@ -1094,17 +1100,17 @@ func (gs *LDKService) GetLogOutput(ctx context.Context, maxLen int) ([]byte, err // naturally sort by date. lastLogFileName := slices.Max(allLogFiles) - logData, err := ReadFileTail(lastLogFileName, maxLen) + logData, err := utils.ReadFileTail(lastLogFileName, maxLen) if err != nil { - gs.svc.Logger.WithError(err).Error("GetLogOutput failed to read log file") + ls.logger.WithError(err).Error("GetLogOutput failed to read log file") return nil, err } return logData, nil } -func (ls *LDKService) logLdkEvent(ctx context.Context, event *ldk_node.Event) { - ls.svc.Logger.WithFields(logrus.Fields{ +func (ls *LDKService) handleLdkEvent(ctx context.Context, event *ldk_node.Event) { + ls.logger.WithFields(logrus.Fields{ "event": event, }).Info("Received LDK event") @@ -1141,7 +1147,7 @@ func (ls *LDKService) logLdkEvent(ctx context.Context, event *ldk_node.Event) { func (ls *LDKService) GetBalances(ctx context.Context) (*lnclient.BalancesResponse, error) { onchainBalance, err := ls.GetOnchainBalance(ctx) if err != nil { - ls.svc.Logger.WithError(err).Error("Failed to retrieve onchain balance") + ls.logger.WithError(err).Error("Failed to retrieve onchain balance") return nil, err } @@ -1224,3 +1230,7 @@ func (ls *LDKService) GetNodeStatus(ctx context.Context) (nodeStatus *lnclient.N InternalNodeStatus: ls.node.Status(), }, nil } + +func (ls *LDKService) UpdateLastWalletSyncRequest() { + ls.lastWalletSyncRequest = time.Now() +} diff --git a/ldk_event_broadcaster.go b/lnclient/ldk/ldk_event_broadcaster.go similarity index 99% rename from ldk_event_broadcaster.go rename to lnclient/ldk/ldk_event_broadcaster.go index b1b0f17e..a5468fa2 100644 --- a/ldk_event_broadcaster.go +++ b/lnclient/ldk/ldk_event_broadcaster.go @@ -1,4 +1,4 @@ -package main +package ldk import ( "context" diff --git a/lnd.go b/lnclient/lnd/lnd.go similarity index 92% rename from lnd.go rename to lnclient/lnd/lnd.go index 66c0caeb..94f5d04a 100644 --- a/lnd.go +++ b/lnclient/lnd/lnd.go @@ -1,4 +1,4 @@ -package main +package lnd import ( "context" @@ -12,11 +12,12 @@ import ( decodepay "github.com/nbd-wtf/ln-decodepay" - "github.com/getAlby/nostr-wallet-connect/lnd" - "github.com/getAlby/nostr-wallet-connect/models/lnclient" + "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/lnclient/lnd/wrapper" + "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/sirupsen/logrus" - "gorm.io/gorm" + // "gorm.io/gorm" "github.com/lightningnetwork/lnd/lnrpc" ) @@ -24,8 +25,8 @@ import ( // wrap it again :sweat_smile: // todo: drop dependency on lndhub package type LNDService struct { - client *lnd.LNDWrapper - db *gorm.DB + client *wrapper.LNDWrapper + // db *gorm.DB Logger *logrus.Logger } @@ -37,7 +38,7 @@ func (svc *LNDService) GetBalance(ctx context.Context) (balance int64, err error return int64(resp.LocalBalance.Msat), nil } -func (svc *LNDService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []Nip47Transaction, err error) { +func (svc *LNDService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []nip47.Transaction, err error) { // Fetch invoices var invoices []*lnrpc.Invoice if invoiceType == "" || invoiceType == "incoming" { @@ -98,7 +99,7 @@ func (svc *LNDService) ListTransactions(ctx context.Context, from, until, limit, settledAt = &settledAtUnix } - transaction := Nip47Transaction{ + transaction := nip47.Transaction{ Type: "outgoing", Invoice: payment.PaymentRequest, Preimage: payment.PaymentPreimage, @@ -143,7 +144,7 @@ func (svc *LNDService) ListChannels(ctx context.Context) ([]lnclient.Channel, er return channels, nil } -func (svc *LNDService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *Nip47Transaction, err error) { +func (svc *LNDService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *nip47.Transaction, err error) { var descriptionHashBytes []byte if descriptionHash != "" { @@ -174,7 +175,7 @@ func (svc *LNDService) MakeInvoice(ctx context.Context, amount int64, descriptio return transaction, nil } -func (svc *LNDService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *Nip47Transaction, err error) { +func (svc *LNDService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *nip47.Transaction, err error) { paymentHashBytes, err := hex.DecodeString(paymentHash) if err != nil || len(paymentHashBytes) != 32 { @@ -193,12 +194,12 @@ func (svc *LNDService) LookupInvoice(ctx context.Context, paymentHash string) (t return transaction, nil } -func (svc *LNDService) SendPaymentSync(ctx context.Context, payReq string) (*lnclient.Nip47PayInvoiceResponse, error) { +func (svc *LNDService) SendPaymentSync(ctx context.Context, payReq string) (*lnclient.PayInvoiceResponse, error) { resp, err := svc.client.SendPaymentSync(ctx, &lnrpc.SendRequest{PaymentRequest: payReq}) if err != nil { return nil, err } - return &lnclient.Nip47PayInvoiceResponse{ + return &lnclient.PayInvoiceResponse{ Preimage: hex.EncodeToString(resp.PaymentPreimage), }, nil } @@ -302,18 +303,18 @@ func makePreimageHex() ([]byte, error) { return bytes, nil } -func NewLNDService(ctx context.Context, svc *Service, lndAddress, lndCertHex, lndMacaroonHex string) (result lnclient.LNClient, err error) { +func NewLNDService(ctx context.Context, logger *logrus.Logger, lndAddress, lndCertHex, lndMacaroonHex string) (result lnclient.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{ + lndClient, err := wrapper.NewLNDclient(wrapper.LNDoptions{ Address: lndAddress, CertHex: lndCertHex, MacaroonHex: lndMacaroonHex, }) if err != nil { - svc.Logger.Errorf("Failed to create new LND client %v", err) + logger.Errorf("Failed to create new LND client %v", err) return nil, err } info, err := lndClient.GetInfo(ctx, &lnrpc.GetInfoRequest{}) @@ -321,9 +322,9 @@ func NewLNDService(ctx context.Context, svc *Service, lndAddress, lndCertHex, ln return nil, err } - lndService := &LNDService{client: lndClient, Logger: svc.Logger, db: svc.db} + lndService := &LNDService{client: lndClient, Logger: logger} - svc.Logger.Infof("Connected to LND - alias %s", info.Alias) + logger.Infof("Connected to LND - alias %s", info.Alias) return lndService, nil } @@ -406,7 +407,7 @@ func (svc *LNDService) GetBalances(ctx context.Context) (*lnclient.BalancesRespo }, nil } -func lndInvoiceToTransaction(invoice *lnrpc.Invoice) *Nip47Transaction { +func lndInvoiceToTransaction(invoice *lnrpc.Invoice) *nip47.Transaction { var settledAt *int64 var preimage string if invoice.State == lnrpc.Invoice_SETTLED { @@ -420,7 +421,7 @@ func lndInvoiceToTransaction(invoice *lnrpc.Invoice) *Nip47Transaction { expiresAt = &expiresAtUnix } - return &Nip47Transaction{ + return &nip47.Transaction{ Type: "incoming", Invoice: invoice.PaymentRequest, Description: invoice.Memo, @@ -451,3 +452,5 @@ func (svc *LNDService) GetNodeStatus(ctx context.Context) (nodeStatus *lnclient. func (svc *LNDService) GetNetworkGraph(nodeIds []string) (lnclient.NetworkGraphResponse, error) { return nil, nil } + +func (svc *LNDService) UpdateLastWalletSyncRequest() {} diff --git a/lnd/interface.go b/lnclient/lnd/wrapper/interface.go similarity index 99% rename from lnd/interface.go rename to lnclient/lnd/wrapper/interface.go index 8bb59797..601f0f7e 100644 --- a/lnd/interface.go +++ b/lnclient/lnd/wrapper/interface.go @@ -1,4 +1,4 @@ -package lnd +package wrapper import ( "context" diff --git a/lnd/lnd.go b/lnclient/lnd/wrapper/lnd.go similarity index 99% rename from lnd/lnd.go rename to lnclient/lnd/wrapper/lnd.go index c188f1df..15e14d23 100644 --- a/lnd/lnd.go +++ b/lnclient/lnd/wrapper/lnd.go @@ -1,4 +1,4 @@ -package lnd +package wrapper import ( "context" diff --git a/models/lnclient/lnclient.go b/lnclient/models.go similarity index 97% rename from models/lnclient/lnclient.go rename to lnclient/models.go index 91b4e51c..930fdb21 100644 --- a/models/lnclient/lnclient.go +++ b/lnclient/models.go @@ -42,7 +42,7 @@ type NodeConnectionInfo struct { } type LNClient interface { - SendPaymentSync(ctx context.Context, payReq string) (*Nip47PayInvoiceResponse, error) + SendPaymentSync(ctx context.Context, payReq string) (*PayInvoiceResponse, error) SendKeysend(ctx context.Context, amount int64, destination, preimage string, customRecords []TLVRecord) (preImage string, err error) GetBalance(ctx context.Context) (balance int64, err error) GetInfo(ctx context.Context) (info *NodeInfo, err error) @@ -68,6 +68,7 @@ type LNClient interface { SignMessage(ctx context.Context, message string) (string, error) GetStorageDir() (string, error) GetNetworkGraph(nodeIds []string) (NetworkGraphResponse, error) + UpdateLastWalletSyncRequest() } type Channel struct { @@ -133,7 +134,7 @@ type LightningBalanceResponse struct { NextMaxReceivableMPP int64 `json:"nextMaxReceivableMPP"` } -type Nip47PayInvoiceResponse struct { +type PayInvoiceResponse struct { Preimage string `json:"preimage"` Fee *uint64 `json:"fee"` } diff --git a/phoenixd.go b/lnclient/phoenixd/phoenixd.go similarity index 94% rename from phoenixd.go rename to lnclient/phoenixd/phoenixd.go index 496271b5..c5803cfb 100644 --- a/phoenixd.go +++ b/lnclient/phoenixd/phoenixd.go @@ -1,4 +1,4 @@ -package main +package phoenixd import ( "context" @@ -12,10 +12,10 @@ import ( "strings" "time" - "github.com/getAlby/nostr-wallet-connect/models/lnclient" + "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/sirupsen/logrus" - "gorm.io/gorm" ) type InvoiceResponse struct { @@ -67,13 +67,12 @@ type BalanceResponse struct { type PhoenixService struct { Address string Authorization string - db *gorm.DB Logger *logrus.Logger } -func NewPhoenixService(svc *Service, address string, authorization string) (result lnclient.LNClient, err error) { +func NewPhoenixService(logger *logrus.Logger, address string, authorization string) (result lnclient.LNClient, err error) { authorizationBase64 := b64.StdEncoding.EncodeToString([]byte(":" + authorization)) - phoenixService := &PhoenixService{Logger: svc.Logger, db: svc.db, Address: address, Authorization: authorizationBase64} + phoenixService := &PhoenixService{Logger: logger, Address: address, Authorization: authorizationBase64} return phoenixService, nil } @@ -122,7 +121,7 @@ func (svc *PhoenixService) GetBalances(ctx context.Context) (*lnclient.BalancesR }, nil } -func (svc *PhoenixService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []Nip47Transaction, err error) { +func (svc *PhoenixService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []nip47.Transaction, err error) { incomingQuery := url.Values{} if from != 0 { incomingQuery.Add("from", strconv.FormatUint(from*1000, 10)) @@ -160,14 +159,14 @@ func (svc *PhoenixService) ListTransactions(ctx context.Context, from, until, li if err := json.NewDecoder(incomingResp.Body).Decode(&incomingPayments); err != nil { return nil, err } - transactions = []Nip47Transaction{} + transactions = []nip47.Transaction{} for _, invoice := range incomingPayments { var settledAt *int64 if invoice.CompletedAt != 0 { settledAtUnix := time.UnixMilli(invoice.CompletedAt).Unix() settledAt = &settledAtUnix } - transaction := Nip47Transaction{ + transaction := nip47.Transaction{ Type: "incoming", Invoice: invoice.Invoice, Preimage: invoice.Preimage, @@ -223,7 +222,7 @@ func (svc *PhoenixService) ListTransactions(ctx context.Context, from, until, li settledAtUnix := time.UnixMilli(invoice.CompletedAt).Unix() settledAt = &settledAtUnix } - transaction := Nip47Transaction{ + transaction := nip47.Transaction{ Type: "outgoing", Invoice: invoice.Invoice, Preimage: invoice.Preimage, @@ -276,7 +275,7 @@ func (svc *PhoenixService) ListChannels(ctx context.Context) ([]lnclient.Channel return channels, nil } -func (svc *PhoenixService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *Nip47Transaction, err error) { +func (svc *PhoenixService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *nip47.Transaction, err error) { form := url.Values{} amountSat := strconv.FormatInt(amount/1000, 10) form.Add("amountSat", amountSat) @@ -313,7 +312,7 @@ func (svc *PhoenixService) MakeInvoice(ctx context.Context, amount int64, descri } expiresAt := time.Now().Add(1 * time.Hour).Unix() - tx := &Nip47Transaction{ + tx := &nip47.Transaction{ Type: "incoming", Invoice: invoiceRes.Serialized, Preimage: "", @@ -327,7 +326,7 @@ func (svc *PhoenixService) MakeInvoice(ctx context.Context, amount int64, descri return tx, nil } -func (svc *PhoenixService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *Nip47Transaction, err error) { +func (svc *PhoenixService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *nip47.Transaction, err error) { req, err := http.NewRequest(http.MethodGet, svc.Address+"/payments/incoming/"+paymentHash, nil) if err != nil { return nil, err @@ -350,7 +349,7 @@ func (svc *PhoenixService) LookupInvoice(ctx context.Context, paymentHash string settledAtUnix := time.UnixMilli(invoiceRes.CompletedAt).Unix() settledAt = &settledAtUnix } - transaction = &Nip47Transaction{ + transaction = &nip47.Transaction{ Type: "incoming", Invoice: invoiceRes.Invoice, Preimage: invoiceRes.Preimage, @@ -364,7 +363,7 @@ func (svc *PhoenixService) LookupInvoice(ctx context.Context, paymentHash string return transaction, nil } -func (svc *PhoenixService) SendPaymentSync(ctx context.Context, payReq string) (*lnclient.Nip47PayInvoiceResponse, error) { +func (svc *PhoenixService) SendPaymentSync(ctx context.Context, payReq string) (*lnclient.PayInvoiceResponse, error) { form := url.Values{} form.Add("invoice", payReq) req, err := http.NewRequest(http.MethodPost, svc.Address+"/payinvoice", strings.NewReader(form.Encode())) @@ -386,7 +385,7 @@ func (svc *PhoenixService) SendPaymentSync(ctx context.Context, payReq string) ( } fee := uint64(payRes.RoutingFeeSat) * 1000 - return &lnclient.Nip47PayInvoiceResponse{ + return &lnclient.PayInvoiceResponse{ Preimage: payRes.PaymentPreimage, Fee: &fee, }, nil @@ -480,3 +479,5 @@ func (svc *PhoenixService) GetStorageDir() (string, error) { func (svc *PhoenixService) GetNetworkGraph(nodeIds []string) (lnclient.NetworkGraphResponse, error) { return nil, nil } + +func (svc *PhoenixService) UpdateLastWalletSyncRequest() {} diff --git a/lsp/lsp_service.go b/lsp/lsp_service.go new file mode 100644 index 00000000..197ba292 --- /dev/null +++ b/lsp/lsp_service.go @@ -0,0 +1,621 @@ +package lsp + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "regexp" + "strconv" + "strings" + "time" + + "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/service" + "github.com/sirupsen/logrus" +) + +type lspService struct { + svc service.Service + logger *logrus.Logger +} + +type lspConnectionInfo struct { + Pubkey string + Address string + Port uint16 +} + +func NewLSPService(svc service.Service, logger *logrus.Logger) *lspService { + return &lspService{ + svc: svc, + logger: logger, + } +} + +func (ls *lspService) NewInstantChannelInvoice(ctx context.Context, request *NewInstantChannelInvoiceRequest) (*NewInstantChannelInvoiceResponse, error) { + var selectedLsp LSP + switch request.LSP { + case "VOLTAGE": + selectedLsp = VoltageLSP() + case "OLYMPUS_FLOW_2_0": + selectedLsp = OlympusLSP() + case "OLYMPUS_MUTINYNET_FLOW_2_0": + selectedLsp = OlympusMutinynetFlowLSP() + case "OLYMPUS_MUTINYNET_LSPS1": + selectedLsp = OlympusMutinynetLSPS1LSP() + case "ALBY": + selectedLsp = AlbyPlebsLSP() + case "ALBY_MUTINYNET": + selectedLsp = AlbyMutinynetPlebsLSP() + default: + return nil, errors.New("unknown LSP") + } + + if ls.svc.GetLNClient() == nil { + return nil, errors.New("LNClient not started") + } + + ls.logger.Infoln("Requesting LSP info") + + var lspInfo *lspConnectionInfo + var err error + switch selectedLsp.LspType { + case LSP_TYPE_FLOW_2_0: + fallthrough + case LSP_TYPE_PMLSP: + lspInfo, err = ls.getFlowLSPInfo(selectedLsp.Url + "/info") + + case LSP_TYPE_LSPS1: + lspInfo, err = ls.getLSPS1LSPInfo(selectedLsp.Url + "/get_info") + + default: + return nil, fmt.Errorf("unsupported LSP type: %v", selectedLsp.LspType) + } + if err != nil { + ls.logger.WithError(err).Error("Failed to request LSP info") + return nil, err + } + + ls.logger.Infoln("Requesting own node info") + + nodeInfo, err := ls.svc.GetLNClient().GetInfo(ctx) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": selectedLsp.Url, + }).Error("Failed to request own node info", err) + return nil, err + } + + ls.logger.WithField("lspInfo", lspInfo).Info("Connecting to LSP node as a peer") + + err = ls.svc.GetLNClient().ConnectPeer(ctx, &lnclient.ConnectPeerRequest{ + Pubkey: lspInfo.Pubkey, + Address: lspInfo.Address, + Port: lspInfo.Port, + }) + + if err != nil { + ls.logger.WithError(err).Error("Failed to connect to peer") + return nil, err + } + + invoice := "" + var fee uint64 = 0 + + switch selectedLsp.LspType { + case LSP_TYPE_FLOW_2_0: + invoice, fee, err = ls.requestFlow20WrappedInvoice(ctx, &selectedLsp, request.Amount, nodeInfo.Pubkey) + case LSP_TYPE_PMLSP: + invoice, fee, err = ls.requestPMLSPInvoice(&selectedLsp, request.Amount, nodeInfo.Pubkey) + case LSP_TYPE_LSPS1: + invoice, fee, err = ls.requestLSPS1Invoice(ctx, &selectedLsp, request.Amount, nodeInfo.Pubkey) + + default: + return nil, fmt.Errorf("unsupported LSP type: %v", selectedLsp.LspType) + } + if err != nil { + ls.logger.WithError(err).Error("Failed to request invoice") + return nil, err + } + + newChannelResponse := &NewInstantChannelInvoiceResponse{ + Invoice: invoice, + Fee: fee, + } + + ls.logger.WithFields(logrus.Fields{ + "newChannelResponse": newChannelResponse, + }).Info("New Channel response") + + return newChannelResponse, nil +} + +func (ls *lspService) getLSPS1LSPInfo(url string) (*lspConnectionInfo, error) { + type LSPS1LSPInfo struct { + // TODO: implement options + Options interface{} `json:"options"` + URIs []string `json:"uris"` + } + var lsps1LspInfo LSPS1LSPInfo + client := http.Client{ + Timeout: time.Second * 10, + } + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": url, + }).Error("Failed to create lsp info request") + return nil, err + } + + res, err := client.Do(req) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": url, + }).Error("Failed to request lsp info") + return nil, err + } + + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": url, + }).Error("Failed to read response body") + return nil, errors.New("failed to read response body") + } + + err = json.Unmarshal(body, &lsps1LspInfo) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": url, + }).Error("Failed to deserialize json") + return nil, fmt.Errorf("failed to deserialize json %s %s", url, string(body)) + } + + uri := lsps1LspInfo.URIs[0] + + // make sure it's a valid IPv4 URI + regex := regexp.MustCompile(`^([0-9a-f]+)@([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+):([0-9]+)$`) + parts := regex.FindStringSubmatch(uri) + ls.logger.WithField("parts", parts).Info("Split URI") + if parts == nil || len(parts) != 4 { + ls.logger.WithField("parts", parts).Info("Unsupported URI") + return nil, errors.New("could not decode LSP URI") + } + + port, err := strconv.Atoi(parts[3]) + if err != nil { + ls.logger.WithField("port", parts[3]).WithError(err).Info("Failed to decode port number") + + return nil, err + } + + return &lspConnectionInfo{ + Pubkey: parts[1], + Address: parts[2], + Port: uint16(port), + }, nil +} +func (ls *lspService) getFlowLSPInfo(url string) (*lspConnectionInfo, error) { + type FlowLSPConnectionMethod struct { + Address string `json:"address"` + Port uint16 `json:"port"` + Type string `json:"type"` + } + type FlowLSPInfo struct { + Pubkey string `json:"pubkey"` + ConnectionMethods []FlowLSPConnectionMethod `json:"connection_methods"` + } + var flowLspInfo FlowLSPInfo + client := http.Client{ + Timeout: time.Second * 10, + } + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": url, + }).Error("Failed to create lsp info request") + return nil, err + } + + res, err := client.Do(req) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": url, + }).Error("Failed to request lsp info") + return nil, err + } + + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": url, + }).Error("Failed to read response body") + return nil, errors.New("failed to read response body") + } + + err = json.Unmarshal(body, &flowLspInfo) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": url, + }).Error("Failed to deserialize json") + return nil, fmt.Errorf("failed to deserialize json %s %s", url, string(body)) + } + + ipIndex := -1 + for i, cm := range flowLspInfo.ConnectionMethods { + if strings.HasPrefix(cm.Type, "ip") { + ipIndex = i + break + } + } + + if ipIndex == -1 { + ls.logger.Error("No ipv4/ipv6 connection method found in LSP info") + return nil, errors.New("unexpected LSP connection method") + } + + return &lspConnectionInfo{ + Pubkey: flowLspInfo.Pubkey, + Address: flowLspInfo.ConnectionMethods[ipIndex].Address, + Port: flowLspInfo.ConnectionMethods[ipIndex].Port, + }, nil +} + +func (ls *lspService) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *LSP, amount uint64, pubkey string) (invoice string, fee uint64, err error) { + ls.logger.Infoln("Requesting fee information") + + type FeeRequest struct { + AmountMsat uint64 `json:"amount_msat"` + Pubkey string `json:"pubkey"` + } + type FeeResponse struct { + FeeAmountMsat uint64 `json:"fee_amount_msat"` + Id string `json:"id"` + } + + var feeResponse FeeResponse + { + client := http.Client{ + Timeout: time.Second * 10, + } + payloadBytes, err := json.Marshal(FeeRequest{ + AmountMsat: amount * 1000, + Pubkey: pubkey, + }) + if err != nil { + return "", 0, err + } + bodyReader := bytes.NewReader(payloadBytes) + + req, err := http.NewRequest(http.MethodPost, selectedLsp.Url+"/fee", bodyReader) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": selectedLsp.Url, + }).Error("Failed to create lsp fee request") + return "", 0, err + } + + req.Header.Set("Content-Type", "application/json") + + res, err := client.Do(req) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": selectedLsp.Url, + }).Error("Failed to request lsp fee") + return "", 0, err + } + + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": selectedLsp.Url, + }).Error("Failed to read response body") + return "", 0, errors.New("failed to read response body") + } + + if res.StatusCode >= 300 { + ls.logger.WithFields(logrus.Fields{ + "body": string(body), + "statusCode": res.StatusCode, + }).Error("fee endpoint returned non-success code") + return "", 0, fmt.Errorf("fee endpoint returned non-success code: %s", string(body)) + } + + err = json.Unmarshal(body, &feeResponse) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": selectedLsp.Url, + }).Error("Failed to deserialize json") + return "", 0, fmt.Errorf("failed to deserialize json %s %s", selectedLsp.Url, string(body)) + } + + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": selectedLsp.Url, + "feeResponse": feeResponse, + }).Info("Got fee response") + if feeResponse.Id == "" { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "feeResponse": feeResponse, + }).Error("No fee id in fee response") + return "", 0, fmt.Errorf("no fee id in fee response %v", feeResponse) + } + fee = feeResponse.FeeAmountMsat / 1000 + } + + // because we don't want the sender to pay the fee + // see: https://docs.voltage.cloud/voltage-lsp#gqBqV + makeInvoiceResponse, err := ls.svc.GetLNClient().MakeInvoice(ctx, int64(amount)*1000-int64(feeResponse.FeeAmountMsat), "", "", 60*60) + if err != nil { + ls.logger.WithError(err).Error("Failed to request own invoice") + return "", 0, fmt.Errorf("failed to request own invoice %v", err) + } + + type ProposalRequest struct { + Bolt11 string `json:"bolt11"` + FeeId string `json:"fee_id"` + } + type ProposalResponse struct { + Bolt11 string `json:"jit_bolt11"` + } + + ls.logger.Infoln("Proposing invoice") + + var proposalResponse ProposalResponse + { + client := http.Client{ + Timeout: time.Second * 10, + } + payloadBytes, err := json.Marshal(ProposalRequest{ + Bolt11: makeInvoiceResponse.Invoice, + FeeId: feeResponse.Id, + }) + if err != nil { + return "", 0, err + } + bodyReader := bytes.NewReader(payloadBytes) + + req, err := http.NewRequest(http.MethodPost, selectedLsp.Url+"/proposal", bodyReader) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": selectedLsp.Url, + }).Error("Failed to create lsp fee request") + return "", 0, err + } + + req.Header.Set("Content-Type", "application/json") + + res, err := client.Do(req) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": selectedLsp.Url, + }).Error("Failed to request lsp fee") + return "", 0, err + } + + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": selectedLsp.Url, + }).Error("Failed to read response body") + return "", 0, errors.New("failed to read response body") + } + + if res.StatusCode >= 300 { + ls.logger.WithFields(logrus.Fields{ + "body": string(body), + "statusCode": res.StatusCode, + }).Error("proposal endpoint returned non-success code") + return "", 0, fmt.Errorf("proposal endpoint returned non-success code: %s", string(body)) + } + + err = json.Unmarshal(body, &proposalResponse) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": selectedLsp.Url, + }).Error("Failed to deserialize json") + return "", 0, fmt.Errorf("failed to deserialize json %s %s", selectedLsp.Url, string(body)) + } + ls.logger.WithField("proposalResponse", proposalResponse).Info("Got proposal response") + if proposalResponse.Bolt11 == "" { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": selectedLsp.Url, + "proposalResponse": proposalResponse, + }).Error("No bolt11 in proposal response") + return "", 0, fmt.Errorf("no bolt11 in proposal response %v", proposalResponse) + } + } + invoice = proposalResponse.Bolt11 + + return invoice, fee, nil +} + +func (ls *lspService) requestPMLSPInvoice(selectedLsp *LSP, amount uint64, pubkey string) (invoice string, fee uint64, err error) { + type NewInstantChannelRequest struct { + Amount uint64 `json:"amount"` + Pubkey string `json:"pubkey"` + } + + client := http.Client{ + Timeout: time.Second * 10, + } + payloadBytes, err := json.Marshal(NewInstantChannelRequest{ + Amount: amount, + Pubkey: pubkey, + }) + if err != nil { + return "", 0, err + } + bodyReader := bytes.NewReader(payloadBytes) + + req, err := http.NewRequest(http.MethodPost, selectedLsp.Url+"/new-channel", bodyReader) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": selectedLsp.Url, + }).Error("Failed to create new channel request") + return "", 0, err + } + + req.Header.Set("Content-Type", "application/json") + + res, err := client.Do(req) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": selectedLsp.Url, + }).Error("Failed to request new channel invoice") + return "", 0, err + } + + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": selectedLsp.Url, + }).Error("Failed to read response body") + return "", 0, errors.New("failed to read response body") + } + + if res.StatusCode >= 300 { + ls.logger.WithFields(logrus.Fields{ + "body": string(body), + "statusCode": res.StatusCode, + }).Error("new-channel endpoint returned non-success code") + return "", 0, fmt.Errorf("new-channel endpoint returned non-success code: %s", string(body)) + } + + var newChannelResponse NewInstantChannelResponse + + err = json.Unmarshal(body, &newChannelResponse) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": selectedLsp.Url, + }).Error("Failed to deserialize json") + return "", 0, fmt.Errorf("failed to deserialize json %s %s", selectedLsp.Url, string(body)) + } + + invoice = newChannelResponse.Invoice + fee = newChannelResponse.FeeAmountMsat / 1000 + + return invoice, fee, nil +} + +func (ls *lspService) requestLSPS1Invoice(ctx context.Context, selectedLsp *LSP, amount uint64, pubkey string) (invoice string, fee uint64, err error) { + client := http.Client{ + Timeout: time.Second * 10, + } + + type NewLSPS1ChannelRequest struct { + PublicKey string `json:"public_key"` + LSPBalanceSat string `json:"lsp_balance_sat"` + ClientBalanceSat string `json:"client_balance_sat"` + RequiredChannelConfirmations uint64 `json:"required_channel_confirmations"` + FundingConfirmsWithinBlocks uint64 `json:"funding_confirms_within_blocks"` + ChannelExpiryBlocks uint64 `json:"channel_expiry_blocks"` + Token string `json:"token"` + RefundOnchainAddress string `json:"refund_onchain_address"` + AnnounceChannel bool `json:"announce_channel"` + } + + refundAddress, err := ls.svc.GetLNClient().GetNewOnchainAddress(ctx) + if err != nil { + ls.logger.WithError(err).Error("Failed to request onchain address") + return "", 0, err + } + + newLSPS1ChannelRequest := NewLSPS1ChannelRequest{ + PublicKey: pubkey, + LSPBalanceSat: strconv.FormatUint(amount, 10), + ClientBalanceSat: "0", + RequiredChannelConfirmations: 0, + FundingConfirmsWithinBlocks: 6, + ChannelExpiryBlocks: 13000, // TODO: this should be customizable + Token: "", + RefundOnchainAddress: refundAddress, + AnnounceChannel: false, // TODO: this should be customizable + } + + payloadBytes, err := json.Marshal(newLSPS1ChannelRequest) + if err != nil { + return "", 0, err + } + bodyReader := bytes.NewReader(payloadBytes) + + req, err := http.NewRequest(http.MethodPost, selectedLsp.Url+"/create_order", bodyReader) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": selectedLsp.Url, + }).Error("Failed to create new channel request") + return "", 0, err + } + + req.Header.Set("Content-Type", "application/json") + + res, err := client.Do(req) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": selectedLsp.Url, + }).Error("Failed to request new channel invoice") + return "", 0, err + } + + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": selectedLsp.Url, + }).Error("Failed to read response body") + return "", 0, errors.New("failed to read response body") + } + + if res.StatusCode >= 300 { + ls.logger.WithFields(logrus.Fields{ + "newLSPS1ChannelRequest": newLSPS1ChannelRequest, + "body": string(body), + "statusCode": res.StatusCode, + }).Error("create_order endpoint returned non-success code") + return "", 0, fmt.Errorf("create_order endpoint returned non-success code: %s", string(body)) + } + + type NewLSPS1ChannelPayment struct { + LightningInvoice string `json:"lightning_invoice"` + FeeTotalSat string `json:"fee_total_sat"` + } + type NewLSPS1ChannelResponse struct { + Payment NewLSPS1ChannelPayment `json:"payment"` + } + + var newChannelResponse NewLSPS1ChannelResponse + + err = json.Unmarshal(body, &newChannelResponse) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": selectedLsp.Url, + }).Error("Failed to deserialize json") + return "", 0, fmt.Errorf("failed to deserialize json %s %s", selectedLsp.Url, string(body)) + } + + invoice = newChannelResponse.Payment.LightningInvoice + fee, err = strconv.ParseUint(newChannelResponse.Payment.FeeTotalSat, 10, 64) + if err != nil { + ls.logger.WithError(err).WithFields(logrus.Fields{ + "url": selectedLsp.Url, + }).Error("Failed to parse fee") + return "", 0, fmt.Errorf("failed to parse fee %v", err) + } + + return invoice, fee, nil +} diff --git a/models/lsp/lsp.go b/lsp/models.go similarity index 80% rename from models/lsp/lsp.go rename to lsp/models.go index 0fbcff28..0dc0ae10 100644 --- a/models/lsp/lsp.go +++ b/lsp/models.go @@ -1,6 +1,9 @@ -// TODO: move to lsp/models.go package lsp +import ( + "context" +) + type LSP struct { Pubkey string Url string @@ -70,3 +73,17 @@ func AlbyMutinynetPlebsLSP() LSP { } return lsp } + +type LSPService interface { + NewInstantChannelInvoice(ctx context.Context, request *NewInstantChannelInvoiceRequest) (*NewInstantChannelInvoiceResponse, error) +} + +type NewInstantChannelInvoiceRequest struct { + Amount uint64 `json:"amount"` + LSP string `json:"lsp"` +} + +type NewInstantChannelInvoiceResponse struct { + Invoice string `json:"invoice"` + Fee uint64 `json:"fee"` +} diff --git a/main_http.go b/main_http.go index 1779fc70..996cd731 100644 --- a/main_http.go +++ b/main_http.go @@ -8,13 +8,14 @@ package main import ( "context" "fmt" - "net/http" + nethttp "net/http" "os" "os/signal" "syscall" "time" echologrus "github.com/davrux/echo-logrus/v4" + "github.com/getAlby/nostr-wallet-connect/http" "github.com/labstack/echo/v4" log "github.com/sirupsen/logrus" ) @@ -26,26 +27,26 @@ func main() { ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, os.Kill) svc, _ := NewService(ctx) - echologrus.Logger = svc.Logger + echologrus.Logger = svc.logger e := echo.New() //register shared routes - httpSvc := NewHttpService(svc) + httpSvc := http.NewHttpService(svc, svc.logger, svc.db, svc.eventPublisher) httpSvc.RegisterSharedRoutes(e) //start Echo server go func() { - if err := e.Start(fmt.Sprintf(":%v", svc.cfg.Env.Port)); err != nil && err != http.ErrServerClosed { - svc.Logger.Fatalf("shutting down the server: %v", err) + if err := e.Start(fmt.Sprintf(":%v", svc.cfg.GetEnv().Port)); err != nil && err != nethttp.ErrServerClosed { + svc.logger.Fatalf("shutting down the server: %v", err) } }() //handle graceful shutdown <-ctx.Done() - svc.Logger.Infof("Shutting down echo server...") + svc.logger.Infof("Shutting down echo server...") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() e.Shutdown(ctx) - svc.Logger.Info("Echo server exited") - svc.Logger.Info("Waiting for service to exit...") + svc.logger.Info("Echo server exited") + svc.logger.Info("Waiting for service to exit...") svc.wg.Wait() - svc.Logger.Info("Service exited") + svc.logger.Info("Service exited") } diff --git a/main_wails.go b/main_wails.go index e76836cc..4ae8dec5 100644 --- a/main_wails.go +++ b/main_wails.go @@ -18,12 +18,12 @@ func main() { app := NewApp(svc) LaunchWailsApp(app) - svc.Logger.Info("Wails app exited") + svc.logger.Info("Wails app exited") - svc.Logger.Info("Cancelling service context...") + svc.logger.Info("Cancelling service context...") // cancel the service context cancel() - svc.Logger.Info("Waiting for service to exit...") + svc.logger.Info("Waiting for service to exit...") svc.wg.Wait() - svc.Logger.Info("Service exited") + svc.logger.Info("Service exited") } diff --git a/migrations/202403171120_delete_ldk_payments.go b/migrations/202403171120_delete_ldk_payments.go index 33c8b80d..2f8c85a9 100644 --- a/migrations/202403171120_delete_ldk_payments.go +++ b/migrations/202403171120_delete_ldk_payments.go @@ -8,7 +8,7 @@ import ( "database/sql" - "github.com/getAlby/nostr-wallet-connect/models/config" + "github.com/getAlby/nostr-wallet-connect/config" "github.com/go-gormigrate/gormigrate/v2" "github.com/sirupsen/logrus" "gorm.io/gorm" diff --git a/migrations/migrate.go b/migrations/migrate.go index 3ac1007e..a448cdc8 100644 --- a/migrations/migrate.go +++ b/migrations/migrate.go @@ -1,7 +1,7 @@ package migrations import ( - "github.com/getAlby/nostr-wallet-connect/models/config" + "github.com/getAlby/nostr-wallet-connect/config" "github.com/go-gormigrate/gormigrate/v2" "github.com/sirupsen/logrus" "gorm.io/gorm" diff --git a/models.go b/models.go deleted file mode 100644 index d33ce7c3..00000000 --- a/models.go +++ /dev/null @@ -1,204 +0,0 @@ -package main - -import ( - "encoding/json" - "time" - - "github.com/getAlby/nostr-wallet-connect/models/lnclient" -) - -const ( - REQUEST_EVENT_STATE_HANDLER_EXECUTING = "executing" - REQUEST_EVENT_STATE_HANDLER_EXECUTED = "executed" - REQUEST_EVENT_STATE_HANDLER_ERROR = "error" -) -const ( - RESPONSE_EVENT_STATE_PUBLISH_CONFIRMED = "confirmed" - RESPONSE_EVENT_STATE_PUBLISH_FAILED = "failed" - RESPONSE_EVENT_STATE_PUBLISH_UNCONFIRMED = "unconfirmed" -) - -// TODO: move to models/db -type App struct { - ID uint - Name string `validate:"required"` - Description string - NostrPubkey string `validate:"required"` - CreatedAt time.Time - UpdatedAt time.Time -} - -// TODO: move to models/db -type AppPermission struct { - ID uint - AppId uint `validate:"required"` - App App - RequestMethod string `validate:"required"` - MaxAmount int - BudgetRenewal string - ExpiresAt *time.Time - CreatedAt time.Time - UpdatedAt time.Time -} - -// TODO: move to models/db -type RequestEvent struct { - ID uint - AppId *uint - App App - NostrId string `validate:"required"` - Content string - State string - CreatedAt time.Time - UpdatedAt time.Time -} - -// TODO: move to models/db -type ResponseEvent struct { - ID uint - NostrId string `validate:"required"` - RequestId uint `validate:"required"` - Content string - State string - RepliedAt time.Time - CreatedAt time.Time - UpdatedAt time.Time -} - -// TODO: move to models/db -type Payment struct { - ID uint - AppId uint `validate:"required"` - App App - RequestEventId uint `validate:"required"` - RequestEvent RequestEvent - Amount uint // in sats - PaymentRequest string - Preimage *string - CreatedAt time.Time - UpdatedAt time.Time -} - -// TODO: move to models/Nip47 -type Nip47Transaction = lnclient.Transaction - -type PayRequest struct { - Invoice string `json:"invoice"` -} - -// TODO: move to models/Nip47 -type Nip47Request struct { - Method string `json:"method"` - Params json.RawMessage `json:"params"` -} - -type Nip47Response struct { - Error *Nip47Error `json:"error,omitempty"` - Result interface{} `json:"result,omitempty"` - ResultType string `json:"result_type"` -} - -type Nip47Notification struct { - Notification interface{} `json:"notification,omitempty"` - NotificationType string `json:"notification_type"` -} - -type Nip47Error struct { - Code string `json:"code,omitempty"` - Message string `json:"message,omitempty"` -} - -type Nip47PaymentReceivedNotification struct { - Nip47Transaction -} - -type Nip47PayParams struct { - Invoice string `json:"invoice"` -} -type Nip47PayResponse struct { - Preimage string `json:"preimage"` - FeesPaid *uint64 `json:"fees_paid"` -} - -type Nip47MultiPayKeysendParams struct { - Keysends []Nip47MultiPayKeysendElement `json:"keysends"` -} - -type Nip47MultiPayKeysendElement struct { - Nip47KeysendParams - Id string `json:"id"` -} - -type Nip47MultiPayInvoiceParams struct { - Invoices []Nip47MultiPayInvoiceElement `json:"invoices"` -} - -type Nip47MultiPayInvoiceElement struct { - Nip47PayParams - Id string `json:"id"` -} - -type Nip47KeysendParams struct { - Amount int64 `json:"amount"` - Pubkey string `json:"pubkey"` - Preimage string `json:"preimage"` - TLVRecords []lnclient.TLVRecord `json:"tlv_records"` -} - -type Nip47BalanceResponse struct { - Balance int64 `json:"balance"` - MaxAmount int `json:"max_amount"` - BudgetRenewal string `json:"budget_renewal"` -} - -// TODO: move to models/Nip47 -type Nip47GetInfoResponse 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"` -} - -type Nip47MakeInvoiceParams struct { - Amount int64 `json:"amount"` - Description string `json:"description"` - DescriptionHash string `json:"description_hash"` - Expiry int64 `json:"expiry"` -} -type Nip47MakeInvoiceResponse struct { - Nip47Transaction -} - -type Nip47LookupInvoiceParams struct { - Invoice string `json:"invoice"` - PaymentHash string `json:"payment_hash"` -} - -type Nip47LookupInvoiceResponse struct { - Nip47Transaction -} - -type Nip47ListTransactionsParams struct { - From uint64 `json:"from,omitempty"` - Until uint64 `json:"until,omitempty"` - Limit uint64 `json:"limit,omitempty"` - Offset uint64 `json:"offset,omitempty"` - Unpaid bool `json:"unpaid,omitempty"` - Type string `json:"type,omitempty"` -} - -type Nip47ListTransactionsResponse struct { - Transactions []Nip47Transaction `json:"transactions"` -} - -type Nip47SignMessageParams struct { - Message string `json:"message"` -} - -type Nip47SignMessageResponse struct { - Message string `json:"message"` - Signature string `json:"signature"` -} diff --git a/models/db/db.go b/models/db/db.go deleted file mode 100644 index 1a8fdf5c..00000000 --- a/models/db/db.go +++ /dev/null @@ -1,13 +0,0 @@ -// TODO: move to db/models.go -package db - -import "time" - -type UserConfig struct { - ID uint - Key string - Value string - Encrypted bool - CreatedAt time.Time - UpdatedAt time.Time -} diff --git a/nip47/models.go b/nip47/models.go index 301b583b..d2cabf98 100644 --- a/nip47/models.go +++ b/nip47/models.go @@ -1,5 +1,11 @@ package nip47 +import ( + "encoding/json" + + "github.com/getAlby/nostr-wallet-connect/lnclient" +) + const ( INFO_EVENT_KIND = 13194 REQUEST_KIND = 23194 @@ -44,3 +50,124 @@ const ( BUDGET_RENEWAL_YEARLY = "yearly" BUDGET_RENEWAL_NEVER = "never" ) + +type Transaction = lnclient.Transaction + +type PayRequest struct { + Invoice string `json:"invoice"` +} + +type Request struct { + Method string `json:"method"` + Params json.RawMessage `json:"params"` +} + +type Response struct { + Error *Error `json:"error,omitempty"` + Result interface{} `json:"result,omitempty"` + ResultType string `json:"result_type"` +} + +type Notification struct { + Notification interface{} `json:"notification,omitempty"` + NotificationType string `json:"notification_type"` +} + +type Error struct { + Code string `json:"code,omitempty"` + Message string `json:"message,omitempty"` +} + +type PaymentReceivedNotification struct { + Transaction +} + +type PayParams struct { + Invoice string `json:"invoice"` +} +type PayResponse struct { + Preimage string `json:"preimage"` + FeesPaid *uint64 `json:"fees_paid"` +} + +type MultiPayKeysendParams struct { + Keysends []MultiPayKeysendElement `json:"keysends"` +} + +type MultiPayKeysendElement struct { + KeysendParams + Id string `json:"id"` +} + +type MultiPayInvoiceParams struct { + Invoices []MultiPayInvoiceElement `json:"invoices"` +} + +type MultiPayInvoiceElement struct { + PayParams + Id string `json:"id"` +} + +type KeysendParams struct { + Amount int64 `json:"amount"` + Pubkey string `json:"pubkey"` + Preimage string `json:"preimage"` + TLVRecords []lnclient.TLVRecord `json:"tlv_records"` +} + +type BalanceResponse struct { + Balance int64 `json:"balance"` + MaxAmount int `json:"max_amount"` + BudgetRenewal string `json:"budget_renewal"` +} + +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"` +} + +type MakeInvoiceParams struct { + Amount int64 `json:"amount"` + Description string `json:"description"` + DescriptionHash string `json:"description_hash"` + Expiry int64 `json:"expiry"` +} +type MakeInvoiceResponse struct { + Transaction +} + +type LookupInvoiceParams struct { + Invoice string `json:"invoice"` + PaymentHash string `json:"payment_hash"` +} + +type LookupInvoiceResponse struct { + Transaction +} + +type ListTransactionsParams struct { + From uint64 `json:"from,omitempty"` + Until uint64 `json:"until,omitempty"` + Limit uint64 `json:"limit,omitempty"` + Offset uint64 `json:"offset,omitempty"` + Unpaid bool `json:"unpaid,omitempty"` + Type string `json:"type,omitempty"` +} + +type ListTransactionsResponse struct { + Transactions []Transaction `json:"transactions"` +} + +type SignMessageParams struct { + Message string `json:"message"` +} + +type SignMessageResponse struct { + Message string `json:"message"` + Signature string `json:"signature"` +} diff --git a/nip47_notifier.go b/nip47_notifier.go index 50b1452c..98ffc1d6 100644 --- a/nip47_notifier.go +++ b/nip47_notifier.go @@ -1,3 +1,4 @@ +// TODO: move to nip47 package main import ( @@ -5,6 +6,7 @@ import ( "encoding/json" "errors" + "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/events" "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/nbd-wtf/go-nostr" @@ -39,28 +41,28 @@ func (notifier *Nip47Notifier) ConsumeEvent(ctx context.Context, event *events.E paymentReceivedEventProperties, ok := event.Properties.(*events.PaymentReceivedEventProperties) if !ok { - notifier.svc.Logger.WithField("event", event).Error("Failed to cast event") + notifier.svc.logger.WithField("event", event).Error("Failed to cast event") return errors.New("failed to cast event") } transaction, err := notifier.svc.lnClient.LookupInvoice(ctx, paymentReceivedEventProperties.PaymentHash) if err != nil { - notifier.svc.Logger. + notifier.svc.logger. WithField("paymentHash", paymentReceivedEventProperties.PaymentHash). WithError(err). Error("Failed to lookup invoice by payment hash") return err } - notifier.notifySubscribers(ctx, &Nip47Notification{ + notifier.notifySubscribers(ctx, &nip47.Notification{ Notification: transaction, NotificationType: nip47.PAYMENT_RECEIVED_NOTIFICATION, }, nostr.Tags{}) return nil } -func (notifier *Nip47Notifier) notifySubscribers(ctx context.Context, notification *Nip47Notification, tags nostr.Tags) { - apps := []App{} +func (notifier *Nip47Notifier) notifySubscribers(ctx context.Context, notification *nip47.Notification, tags nostr.Tags) { + apps := []db.App{} // TODO: join apps and permissions notifier.svc.db.Find(&apps) @@ -74,15 +76,15 @@ func (notifier *Nip47Notifier) notifySubscribers(ctx context.Context, notificati } } -func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *App, notification *Nip47Notification, tags nostr.Tags) { - notifier.svc.Logger.WithFields(logrus.Fields{ +func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App, notification *nip47.Notification, tags nostr.Tags) { + notifier.svc.logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).Info("Notifying subscriber") - ss, err := nip04.ComputeSharedSecret(app.NostrPubkey, notifier.svc.cfg.NostrSecretKey) + ss, err := nip04.ComputeSharedSecret(app.NostrPubkey, notifier.svc.cfg.GetNostrSecretKey()) if err != nil { - notifier.svc.Logger.WithFields(logrus.Fields{ + notifier.svc.logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).WithError(err).Error("Failed to compute shared secret") @@ -91,7 +93,7 @@ func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *App, n payloadBytes, err := json.Marshal(notification) if err != nil { - notifier.svc.Logger.WithFields(logrus.Fields{ + notifier.svc.logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).WithError(err).Error("Failed to stringify notification") @@ -99,7 +101,7 @@ func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *App, n } msg, err := nip04.Encrypt(string(payloadBytes), ss) if err != nil { - notifier.svc.Logger.WithFields(logrus.Fields{ + notifier.svc.logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).WithError(err).Error("Failed to encrypt notification payload") @@ -110,15 +112,15 @@ func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *App, n allTags = append(allTags, tags...) event := &nostr.Event{ - PubKey: notifier.svc.cfg.NostrPublicKey, + PubKey: notifier.svc.cfg.GetNostrPublicKey(), CreatedAt: nostr.Now(), Kind: nip47.NOTIFICATION_KIND, Tags: allTags, Content: msg, } - err = event.Sign(notifier.svc.cfg.NostrSecretKey) + err = event.Sign(notifier.svc.cfg.GetNostrSecretKey()) if err != nil { - notifier.svc.Logger.WithFields(logrus.Fields{ + notifier.svc.logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).WithError(err).Error("Failed to sign event") @@ -127,13 +129,13 @@ func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *App, n err = notifier.relay.Publish(ctx, *event) if err != nil { - notifier.svc.Logger.WithFields(logrus.Fields{ + notifier.svc.logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).WithError(err).Error("Failed to publish notification") return } - notifier.svc.Logger.WithFields(logrus.Fields{ + notifier.svc.logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).Info("Published notification event") diff --git a/nip47_notifier_test.go b/nip47_notifier_test.go index b554f5b2..ce156952 100644 --- a/nip47_notifier_test.go +++ b/nip47_notifier_test.go @@ -7,6 +7,7 @@ import ( "os" "testing" + "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/events" "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/nbd-wtf/go-nostr" @@ -24,7 +25,7 @@ func TestSendNotification(t *testing.T) { app, ss, err := createApp(svc) assert.NoError(t, err) - appPermission := &AppPermission{ + appPermission := &db.AppPermission{ AppId: app.ID, App: *app, RequestMethod: nip47.NOTIFICATIONS_PERMISSION, @@ -32,8 +33,8 @@ func TestSendNotification(t *testing.T) { err = svc.db.Create(appPermission).Error assert.NoError(t, err) - svc.nip47NotificationQueue = nip47.NewNip47NotificationQueue(svc.Logger) - svc.EventPublisher.RegisterSubscriber(svc.nip47NotificationQueue) + svc.nip47NotificationQueue = nip47.NewNip47NotificationQueue(svc.logger) + svc.eventPublisher.RegisterSubscriber(svc.nip47NotificationQueue) testEvent := &events.Event{ Event: "nwc_payment_received", @@ -44,7 +45,7 @@ func TestSendNotification(t *testing.T) { }, } - svc.EventPublisher.Publish(testEvent) + svc.eventPublisher.Publish(testEvent) receivedEvent := <-svc.nip47NotificationQueue.Channel() assert.Equal(t, testEvent, receivedEvent) @@ -59,15 +60,15 @@ func TestSendNotification(t *testing.T) { decrypted, err := nip04.Decrypt(relay.publishedEvent.Content, ss) assert.NoError(t, err) - unmarshalledResponse := Nip47Notification{ - Notification: &Nip47PaymentReceivedNotification{}, + unmarshalledResponse := nip47.Notification{ + Notification: &nip47.PaymentReceivedNotification{}, } err = json.Unmarshal([]byte(decrypted), &unmarshalledResponse) assert.NoError(t, err) assert.Equal(t, nip47.PAYMENT_RECEIVED_NOTIFICATION, unmarshalledResponse.NotificationType) - transaction := (unmarshalledResponse.Notification.(*Nip47PaymentReceivedNotification)) + transaction := (unmarshalledResponse.Notification.(*nip47.PaymentReceivedNotification)) assert.Equal(t, mockTransaction.Type, transaction.Type) assert.Equal(t, mockTransaction.Invoice, transaction.Invoice) assert.Equal(t, mockTransaction.Description, transaction.Description) @@ -89,8 +90,8 @@ func TestSendNotificationNoPermission(t *testing.T) { _, _, err = createApp(svc) assert.NoError(t, err) - svc.nip47NotificationQueue = nip47.NewNip47NotificationQueue(svc.Logger) - svc.EventPublisher.RegisterSubscriber(svc.nip47NotificationQueue) + svc.nip47NotificationQueue = nip47.NewNip47NotificationQueue(svc.logger) + svc.eventPublisher.RegisterSubscriber(svc.nip47NotificationQueue) testEvent := &events.Event{ Event: "nwc_payment_received", @@ -101,7 +102,7 @@ func TestSendNotificationNoPermission(t *testing.T) { }, } - svc.EventPublisher.Publish(testEvent) + svc.eventPublisher.Publish(testEvent) receivedEvent := <-svc.nip47NotificationQueue.Channel() assert.Equal(t, testEvent, receivedEvent) diff --git a/service.go b/service.go index de8cfbcf..cc6c75a4 100644 --- a/service.go +++ b/service.go @@ -28,10 +28,17 @@ import ( alby "github.com/getAlby/nostr-wallet-connect/alby" "github.com/getAlby/nostr-wallet-connect/events" + "github.com/getAlby/nostr-wallet-connect/utils" + "github.com/getAlby/nostr-wallet-connect/config" + "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/lnclient/breez" + "github.com/getAlby/nostr-wallet-connect/lnclient/greenlight" + "github.com/getAlby/nostr-wallet-connect/lnclient/ldk" + "github.com/getAlby/nostr-wallet-connect/lnclient/lnd" + "github.com/getAlby/nostr-wallet-connect/lnclient/phoenixd" "github.com/getAlby/nostr-wallet-connect/migrations" - "github.com/getAlby/nostr-wallet-connect/models/config" - "github.com/getAlby/nostr-wallet-connect/models/lnclient" "github.com/getAlby/nostr-wallet-connect/nip47" ) @@ -40,24 +47,25 @@ const ( logFilename = "nwc.log" ) +// TODO: move to service/ +// TODO: do not expose service struct type Service struct { - // config from .env only. Fetch dynamic config from db - cfg *Config + // config from .GetEnv() only. Fetch dynamic config from db + cfg config.Config db *gorm.DB lnClient lnclient.LNClient - Logger *logrus.Logger - AlbyOAuthSvc *alby.AlbyOAuthService - EventPublisher events.EventPublisher + logger *logrus.Logger + albyOAuthSvc alby.AlbyOAuthService + eventPublisher events.EventPublisher ctx context.Context wg *sync.WaitGroup nip47NotificationQueue nip47.Nip47NotificationQueue appCancelFn context.CancelFunc - lastWalletSyncRequest time.Time } // TODO: move to service.go func NewService(ctx context.Context) (*Service, error) { - // Load config from environment variables / .env file + // Load config from environment variables / .GetEnv() file godotenv.Load(".env") appConfig := &config.AppConfig{} err := envconfig.Process("", appConfig) @@ -107,28 +115,27 @@ func NewService(ctx context.Context) (*Service, error) { } } - var db *gorm.DB + var gormDB *gorm.DB var sqlDb *sql.DB - db, err = gorm.Open(sqlite.Open(appConfig.DatabaseUri), &gorm.Config{}) + gormDB, err = gorm.Open(sqlite.Open(appConfig.DatabaseUri), &gorm.Config{}) if err != nil { return nil, err } // Enable foreign keys for sqlite - db.Exec("PRAGMA foreign_keys=ON;") - sqlDb, err = db.DB() + gormDB.Exec("PRAGMA foreign_keys=ON;") + sqlDb, err = gormDB.DB() if err != nil { return nil, err } sqlDb.SetMaxOpenConns(1) - err = migrations.Migrate(db, appConfig, logger) + err = migrations.Migrate(gormDB, appConfig, logger) if err != nil { logger.WithError(err).Error("Failed to migrate") return nil, err } - cfg := &Config{} - cfg.Init(db, appConfig, logger) + cfg := config.NewConfig(gormDB, appConfig, logger) eventPublisher := events.NewEventPublisher(logger) @@ -143,17 +150,16 @@ func NewService(ctx context.Context) (*Service, error) { var wg sync.WaitGroup svc := &Service{ cfg: cfg, - db: db, + db: gormDB, ctx: ctx, wg: &wg, - Logger: logger, - EventPublisher: eventPublisher, + logger: logger, + eventPublisher: eventPublisher, nip47NotificationQueue: nip47NotificationQueue, + albyOAuthSvc: alby.NewAlbyOAuthService(logger, cfg, cfg.GetEnv(), db.NewDBService(gormDB, logger)), } - // FIXME: tangled dependency - svc.AlbyOAuthSvc = alby.NewAlbyOAuthService(logger, cfg, cfg.Env, NewAPI(svc)) - eventPublisher.RegisterSubscriber(svc.AlbyOAuthSvc) + eventPublisher.RegisterSubscriber(svc.albyOAuthSvc) eventPublisher.Publish(&events.Event{ Event: "nwc_started", @@ -164,11 +170,11 @@ func NewService(ctx context.Context) (*Service, error) { func (svc *Service) StopLNClient() error { if svc.lnClient != nil { - svc.Logger.Info("Shutting down LDK client") + svc.logger.Info("Shutting down LDK client") err := svc.lnClient.Shutdown() if err != nil { - svc.Logger.WithError(err).Error("Failed to stop LN backend") - svc.EventPublisher.Publish(&events.Event{ + svc.logger.WithError(err).Error("Failed to stop LN backend") + svc.eventPublisher.Publish(&events.Event{ Event: "nwc_node_stop_failed", Properties: map[string]interface{}{ "error": fmt.Sprintf("%v", err), @@ -176,13 +182,13 @@ func (svc *Service) StopLNClient() error { }) return err } - svc.Logger.Info("Publishing node shutdown event") + svc.logger.Info("Publishing node shutdown event") svc.lnClient = nil - svc.EventPublisher.Publish(&events.Event{ + svc.eventPublisher.Publish(&events.Event{ Event: "nwc_node_stopped", }) } - svc.Logger.Info("LNClient stopped successfully") + svc.logger.Info("LNClient stopped successfully") return nil } @@ -197,51 +203,51 @@ func (svc *Service) launchLNBackend(ctx context.Context, encryptionKey string) e return errors.New("no LNBackendType specified") } - svc.Logger.Infof("Launching LN Backend: %s", lnBackend) + svc.logger.Infof("Launching LN Backend: %s", lnBackend) var lnClient lnclient.LNClient switch lnBackend { case config.LNDBackendType: LNDAddress, _ := svc.cfg.Get("LNDAddress", encryptionKey) LNDCertHex, _ := svc.cfg.Get("LNDCertHex", encryptionKey) LNDMacaroonHex, _ := svc.cfg.Get("LNDMacaroonHex", encryptionKey) - lnClient, err = NewLNDService(ctx, svc, LNDAddress, LNDCertHex, LNDMacaroonHex) + lnClient, err = lnd.NewLNDService(ctx, svc.logger, LNDAddress, LNDCertHex, LNDMacaroonHex) case config.LDKBackendType: Mnemonic, _ := svc.cfg.Get("Mnemonic", encryptionKey) - LDKWorkdir := path.Join(svc.cfg.Env.Workdir, "ldk") + LDKWorkdir := path.Join(svc.cfg.GetEnv().Workdir, "ldk") - lnClient, err = NewLDKService(ctx, svc, Mnemonic, LDKWorkdir, svc.cfg.Env.LDKNetwork, svc.cfg.Env.LDKEsploraServer, svc.cfg.Env.LDKGossipSource) + lnClient, err = ldk.NewLDKService(ctx, svc.logger, svc.cfg, svc.eventPublisher, Mnemonic, LDKWorkdir, svc.cfg.GetEnv().LDKNetwork, svc.cfg.GetEnv().LDKEsploraServer, svc.cfg.GetEnv().LDKGossipSource) case config.GreenlightBackendType: Mnemonic, _ := svc.cfg.Get("Mnemonic", encryptionKey) GreenlightInviteCode, _ := svc.cfg.Get("GreenlightInviteCode", encryptionKey) - GreenlightWorkdir := path.Join(svc.cfg.Env.Workdir, "greenlight") + GreenlightWorkdir := path.Join(svc.cfg.GetEnv().Workdir, "greenlight") - lnClient, err = NewGreenlightService(svc, Mnemonic, GreenlightInviteCode, GreenlightWorkdir, encryptionKey) + lnClient, err = greenlight.NewGreenlightService(svc.cfg, svc.logger, Mnemonic, GreenlightInviteCode, GreenlightWorkdir, encryptionKey) case config.BreezBackendType: Mnemonic, _ := svc.cfg.Get("Mnemonic", encryptionKey) BreezAPIKey, _ := svc.cfg.Get("BreezAPIKey", encryptionKey) GreenlightInviteCode, _ := svc.cfg.Get("GreenlightInviteCode", encryptionKey) - BreezWorkdir := path.Join(svc.cfg.Env.Workdir, "breez") + BreezWorkdir := path.Join(svc.cfg.GetEnv().Workdir, "breez") - lnClient, err = NewBreezService(svc.Logger, Mnemonic, BreezAPIKey, GreenlightInviteCode, BreezWorkdir) + lnClient, err = breez.NewBreezService(svc.logger, Mnemonic, BreezAPIKey, GreenlightInviteCode, BreezWorkdir) case config.PhoenixBackendType: - lnClient, err = NewPhoenixService(svc, svc.cfg.Env.PhoenixdAddress, svc.cfg.Env.PhoenixdAuthorization) + lnClient, err = phoenixd.NewPhoenixService(svc.logger, svc.cfg.GetEnv().PhoenixdAddress, svc.cfg.GetEnv().PhoenixdAuthorization) default: - svc.Logger.Fatalf("Unsupported LNBackendType: %v", lnBackend) + svc.logger.Fatalf("Unsupported LNBackendType: %v", lnBackend) } if err != nil { - svc.Logger.WithError(err).Error("Failed to launch LN backend") + svc.logger.WithError(err).Error("Failed to launch LN backend") return err } info, err := lnClient.GetInfo(ctx) if err != nil { - svc.Logger.WithError(err).Error("Failed to fetch node info") + svc.logger.WithError(err).Error("Failed to fetch node info") } if info != nil && info.Pubkey != "" { - svc.EventPublisher.SetGlobalProperty("node_id", info.Pubkey) + svc.eventPublisher.SetGlobalProperty("node_id", info.Pubkey) } - svc.EventPublisher.Publish(&events.Event{ + svc.eventPublisher.Publish(&events.Event{ Event: "nwc_node_started", Properties: map[string]interface{}{ "node_type": lnBackend, @@ -260,7 +266,11 @@ func (svc *Service) createFilters(identityPubkey string) nostr.Filters { } func (svc *Service) noticeHandler(notice string) { - svc.Logger.Infof("Received a notice %s", notice) + svc.logger.Infof("Received a notice %s", notice) +} + +func (svc *Service) GetLNClient() lnclient.LNClient { + return svc.lnClient } func (svc *Service) StartSubscription(ctx context.Context, sub *nostr.Subscription) error { @@ -280,34 +290,34 @@ func (svc *Service) StartSubscription(ctx context.Context, sub *nostr.Subscripti go func() { // block till EOS is received <-sub.EndOfStoredEvents - svc.Logger.Info("Received EOS") + svc.logger.Info("Received EOS") // loop through incoming events for event := range sub.Events { go svc.HandleEvent(ctx, sub, event) } - svc.Logger.Info("Relay subscription events channel ended") + svc.logger.Info("Relay subscription events channel ended") }() <-ctx.Done() if sub.Relay.ConnectionError != nil { - svc.Logger.WithField("connectionError", sub.Relay.ConnectionError).Error("Relay error") + svc.logger.WithField("connectionError", sub.Relay.ConnectionError).Error("Relay error") return sub.Relay.ConnectionError } - svc.Logger.Info("Exiting subscription...") + svc.logger.Info("Exiting subscription...") return nil } -func (svc *Service) PublishEvent(ctx context.Context, sub *nostr.Subscription, requestEvent *RequestEvent, resp *nostr.Event, app *App) error { +func (svc *Service) PublishEvent(ctx context.Context, sub *nostr.Subscription, requestEvent *db.RequestEvent, resp *nostr.Event, app *db.App) error { var appId *uint if app != nil { appId = &app.ID } - responseEvent := ResponseEvent{NostrId: resp.ID, RequestId: requestEvent.ID, Content: resp.Content, State: "received"} + responseEvent := db.ResponseEvent{NostrId: resp.ID, RequestId: requestEvent.ID, Content: resp.Content, State: "received"} err := svc.db.Create(&responseEvent).Error if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": appId, "replyEventId": resp.ID, @@ -317,8 +327,8 @@ func (svc *Service) PublishEvent(ctx context.Context, sub *nostr.Subscription, r err = sub.Relay.Publish(ctx, *resp) if err != nil { - responseEvent.State = RESPONSE_EVENT_STATE_PUBLISH_FAILED - svc.Logger.WithFields(logrus.Fields{ + responseEvent.State = db.RESPONSE_EVENT_STATE_PUBLISH_FAILED + svc.logger.WithFields(logrus.Fields{ "requestEventId": requestEvent.ID, "requestNostrEventId": requestEvent.NostrId, "appId": appId, @@ -326,9 +336,9 @@ func (svc *Service) PublishEvent(ctx context.Context, sub *nostr.Subscription, r "responseNostrEventId": resp.ID, }).Errorf("Failed to publish reply: %v", err) } else { - responseEvent.State = RESPONSE_EVENT_STATE_PUBLISH_CONFIRMED + responseEvent.State = db.RESPONSE_EVENT_STATE_PUBLISH_CONFIRMED responseEvent.RepliedAt = time.Now() - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventId": requestEvent.ID, "requestNostrEventId": requestEvent.NostrId, "appId": appId, @@ -339,7 +349,7 @@ func (svc *Service) PublishEvent(ctx context.Context, sub *nostr.Subscription, r err = svc.db.Save(&responseEvent).Error if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventId": requestEvent.ID, "requestNostrEventId": requestEvent.NostrId, "appId": appId, @@ -353,25 +363,15 @@ func (svc *Service) PublishEvent(ctx context.Context, sub *nostr.Subscription, r } func (svc *Service) HandleEvent(ctx context.Context, sub *nostr.Subscription, event *nostr.Event) { - var nip47Response *Nip47Response - svc.Logger.WithFields(logrus.Fields{ + var nip47Response *nip47.Response + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, }).Info("Processing Event") - // make sure we don't know the event, yet - requestEvent := RequestEvent{} - findEventResult := svc.db.Where("nostr_id = ?", event.ID).Find(&requestEvent) - if findEventResult.RowsAffected != 0 { - svc.Logger.WithFields(logrus.Fields{ - "requestEventNostrId": event.ID, - }).Warn("Event already processed") - return - } - - ss, err := nip04.ComputeSharedSecret(event.PubKey, svc.cfg.NostrSecretKey) + ss, err := nip04.ComputeSharedSecret(event.PubKey, svc.cfg.GetNostrSecretKey()) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, }).Errorf("Failed to compute shared secret: %v", err) @@ -379,22 +379,28 @@ func (svc *Service) HandleEvent(ctx context.Context, sub *nostr.Subscription, ev } // store request event - requestEvent = RequestEvent{AppId: nil, NostrId: event.ID, Content: event.Content, State: REQUEST_EVENT_STATE_HANDLER_EXECUTING} + requestEvent := db.RequestEvent{AppId: nil, NostrId: event.ID, Content: event.Content, State: db.REQUEST_EVENT_STATE_HANDLER_EXECUTING} err = svc.db.Create(&requestEvent).Error if err != nil { - svc.Logger.WithFields(logrus.Fields{ + if errors.Is(err, gorm.ErrDuplicatedKey) { + svc.logger.WithFields(logrus.Fields{ + "requestEventNostrId": event.ID, + }).Warn("Event already processed") + return + } + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, }).Errorf("Failed to save nostr event: %v", err) - nip47Response = &Nip47Response{ - Error: &Nip47Error{ + nip47Response = &nip47.Response{ + Error: &nip47.Error{ Code: nip47.ERROR_INTERNAL, Message: fmt.Sprintf("Failed to save nostr event: %s", err.Error()), }, } resp, err := svc.createResponse(event, nip47Response, nostr.Tags{}, ss) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, }).Errorf("Failed to process event: %v", err) @@ -403,34 +409,34 @@ func (svc *Service) HandleEvent(ctx context.Context, sub *nostr.Subscription, ev return } - app := App{} - err = svc.db.First(&app, &App{ + app := db.App{} + err = svc.db.First(&app, &db.App{ NostrPubkey: event.PubKey, }).Error if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "nostrPubkey": event.PubKey, }).Errorf("Failed to find app for nostr pubkey: %v", err) - nip47Response = &Nip47Response{ - Error: &Nip47Error{ + nip47Response = &nip47.Response{ + Error: &nip47.Error{ Code: nip47.ERROR_UNAUTHORIZED, Message: "The public key does not have a wallet connected.", }, } resp, err := svc.createResponse(event, nip47Response, nostr.Tags{}, ss) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, }).Errorf("Failed to process event: %v", err) } svc.PublishEvent(ctx, sub, &requestEvent, resp, &app) - requestEvent.State = REQUEST_EVENT_STATE_HANDLER_ERROR + requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR err = svc.db.Save(&requestEvent).Error if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "nostrPubkey": event.PubKey, }).Errorf("Failed to save state to nostr event: %v", err) } @@ -440,29 +446,29 @@ func (svc *Service) HandleEvent(ctx context.Context, sub *nostr.Subscription, ev requestEvent.AppId = &app.ID err = svc.db.Save(&requestEvent).Error if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "nostrPubkey": event.PubKey, }).Errorf("Failed to save app to nostr event: %v", err) - nip47Response = &Nip47Response{ - Error: &Nip47Error{ + nip47Response = &nip47.Response{ + Error: &nip47.Error{ Code: nip47.ERROR_UNAUTHORIZED, Message: fmt.Sprintf("Failed to save app to nostr event: %s", err.Error()), }, } resp, err := svc.createResponse(event, nip47Response, nostr.Tags{}, ss) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, }).Errorf("Failed to process event: %v", err) } svc.PublishEvent(ctx, sub, &requestEvent, resp, &app) - requestEvent.State = REQUEST_EVENT_STATE_HANDLER_ERROR + requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR err = svc.db.Save(&requestEvent).Error if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "nostrPubkey": event.PubKey, }).Errorf("Failed to save state to nostr event: %v", err) } @@ -470,24 +476,24 @@ func (svc *Service) HandleEvent(ctx context.Context, sub *nostr.Subscription, ev return } - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, "appId": app.ID, }).Info("App found for nostr event") //to be extra safe, decrypt using the key found from the app - ss, err = nip04.ComputeSharedSecret(app.NostrPubkey, svc.cfg.NostrSecretKey) + ss, err = nip04.ComputeSharedSecret(app.NostrPubkey, svc.cfg.GetNostrSecretKey()) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, }).Errorf("Failed to process event: %v", err) - requestEvent.State = REQUEST_EVENT_STATE_HANDLER_ERROR + requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR err = svc.db.Save(&requestEvent).Error if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "nostrPubkey": event.PubKey, }).Errorf("Failed to save state to nostr event: %v", err) } @@ -496,38 +502,38 @@ func (svc *Service) HandleEvent(ctx context.Context, sub *nostr.Subscription, ev } payload, err := nip04.Decrypt(event.Content, ss) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, "appId": app.ID, }).Errorf("Failed to decrypt content: %v", err) - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, }).Errorf("Failed to process event: %v", err) - requestEvent.State = REQUEST_EVENT_STATE_HANDLER_ERROR + requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR err = svc.db.Save(&requestEvent).Error if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "nostrPubkey": event.PubKey, }).Errorf("Failed to save state to nostr event: %v", err) } return } - nip47Request := &Nip47Request{} + nip47Request := &nip47.Request{} err = json.Unmarshal([]byte(payload), nip47Request) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, }).Errorf("Failed to process event: %v", err) - requestEvent.State = REQUEST_EVENT_STATE_HANDLER_ERROR + requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR err = svc.db.Save(&requestEvent).Error if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "nostrPubkey": event.PubKey, }).Errorf("Failed to save state to nostr event: %v", err) } @@ -537,29 +543,29 @@ func (svc *Service) HandleEvent(ctx context.Context, sub *nostr.Subscription, ev // TODO: replace with a channel // TODO: update all previous occurences of svc.PublishEvent to also use the channel - publishResponse := func(nip47Response *Nip47Response, tags nostr.Tags) { + publishResponse := func(nip47Response *nip47.Response, tags nostr.Tags) { resp, err := svc.createResponse(event, nip47Response, tags, ss) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, }).Errorf("Failed to create response: %v", err) - requestEvent.State = REQUEST_EVENT_STATE_HANDLER_ERROR + requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR } else { err = svc.PublishEvent(ctx, sub, &requestEvent, resp, &app) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, }).Errorf("Failed to publish event: %v", err) - requestEvent.State = REQUEST_EVENT_STATE_HANDLER_ERROR + requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR } else { - requestEvent.State = REQUEST_EVENT_STATE_HANDLER_EXECUTED + requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_EXECUTED } } err = svc.db.Save(&requestEvent).Error if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "nostrPubkey": event.PubKey, }).Errorf("Failed to save state to nostr event: %v", err) } @@ -591,10 +597,10 @@ func (svc *Service) HandleEvent(ctx context.Context, sub *nostr.Subscription, ev } } -func (svc *Service) handleUnknownMethod(ctx context.Context, nip47Request *Nip47Request, publishResponse func(*Nip47Response, nostr.Tags)) { - publishResponse(&Nip47Response{ +func (svc *Service) handleUnknownMethod(ctx context.Context, nip47Request *nip47.Request, publishResponse func(*nip47.Response, nostr.Tags)) { + publishResponse(&nip47.Response{ ResultType: nip47Request.Method, - Error: &Nip47Error{ + Error: &nip47.Error{ Code: nip47.ERROR_NOT_IMPLEMENTED, Message: fmt.Sprintf("Unknown method: %s", nip47Request.Method), }, @@ -615,22 +621,22 @@ func (svc *Service) createResponse(initialEvent *nostr.Event, content interface{ allTags = append(allTags, tags...) resp := &nostr.Event{ - PubKey: svc.cfg.NostrPublicKey, + PubKey: svc.cfg.GetNostrPublicKey(), CreatedAt: nostr.Now(), Kind: nip47.RESPONSE_KIND, Tags: allTags, Content: msg, } - err = resp.Sign(svc.cfg.NostrSecretKey) + err = resp.Sign(svc.cfg.GetNostrSecretKey()) if err != nil { return nil, err } return resp, nil } -func (svc *Service) GetMethods(app *App) []string { - appPermissions := []AppPermission{} - svc.db.Find(&appPermissions, &AppPermission{ +func (svc *Service) GetMethods(app *db.App) []string { + appPermissions := []db.AppPermission{} + svc.db.Find(&appPermissions, &db.AppPermission{ AppId: app.ID, }) requestMethods := make([]string, 0, len(appPermissions)) @@ -645,16 +651,16 @@ func (svc *Service) GetMethods(app *App) []string { return requestMethods } -func (svc *Service) decodeNip47Request(nip47Request *Nip47Request, requestEvent *RequestEvent, app *App, methodParams interface{}) *Nip47Response { +func (svc *Service) decodeNip47Request(nip47Request *nip47.Request, requestEvent *db.RequestEvent, app *db.App, methodParams interface{}) *nip47.Response { err := json.Unmarshal(nip47Request.Params, methodParams) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, }).Errorf("Failed to decode nostr event: %v", err) - return &Nip47Response{ + return &nip47.Response{ ResultType: nip47Request.Method, - Error: &Nip47Error{ + Error: &nip47.Error{ Code: nip47.ERROR_BAD_REQUEST, Message: err.Error(), }} @@ -662,17 +668,17 @@ func (svc *Service) decodeNip47Request(nip47Request *Nip47Request, requestEvent return nil } -func (svc *Service) checkPermission(nip47Request *Nip47Request, requestNostrEventId string, app *App, amount int64) *Nip47Response { +func (svc *Service) checkPermission(nip47Request *nip47.Request, requestNostrEventId string, app *db.App, amount int64) *nip47.Response { hasPermission, code, message := svc.hasPermission(app, nip47Request.Method, amount) if !hasPermission { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestEventNostrId": requestNostrEventId, "appId": app.ID, "code": code, "message": message, }).Error("App does not have permission") - svc.EventPublisher.Publish(&events.Event{ + svc.eventPublisher.Publish(&events.Event{ Event: "nwc_permission_denied", Properties: map[string]interface{}{ "request_method": nip47Request.Method, @@ -683,9 +689,9 @@ func (svc *Service) checkPermission(nip47Request *Nip47Request, requestNostrEven }, }) - return &Nip47Response{ + return &nip47.Response{ ResultType: nip47Request.Method, - Error: &Nip47Error{ + Error: &nip47.Error{ Code: code, Message: message, }, @@ -694,14 +700,14 @@ func (svc *Service) checkPermission(nip47Request *Nip47Request, requestNostrEven return nil } -func (svc *Service) hasPermission(app *App, requestMethod string, amount int64) (result bool, code string, message string) { +func (svc *Service) hasPermission(app *db.App, requestMethod string, amount int64) (result bool, code string, message string) { switch requestMethod { case nip47.PAY_INVOICE_METHOD, nip47.PAY_KEYSEND_METHOD, nip47.MULTI_PAY_INVOICE_METHOD, nip47.MULTI_PAY_KEYSEND_METHOD: requestMethod = nip47.PAY_INVOICE_METHOD } - appPermission := AppPermission{} - findPermissionResult := svc.db.Find(&appPermission, &AppPermission{ + appPermission := db.AppPermission{} + findPermissionResult := svc.db.Find(&appPermission, &db.AppPermission{ AppId: app.ID, RequestMethod: requestMethod, }) @@ -711,7 +717,7 @@ func (svc *Service) hasPermission(app *App, requestMethod string, amount int64) } expiresAt := appPermission.ExpiresAt if expiresAt != nil && expiresAt.Before(time.Now()) { - svc.Logger.WithFields(logrus.Fields{ + svc.logger.WithFields(logrus.Fields{ "requestMethod": requestMethod, "expiresAt": expiresAt.Unix(), "appId": app.ID, @@ -734,12 +740,13 @@ func (svc *Service) hasPermission(app *App, requestMethod string, amount int64) return true, "", "" } -func (svc *Service) GetBudgetUsage(appPermission *AppPermission) int64 { +// TODO: move somewhere else +func (svc *Service) GetBudgetUsage(appPermission *db.AppPermission) int64 { var result struct { Sum uint } // TODO: discard failed payments from this check instead of checking payments that have a preimage - svc.db.Table("payments").Select("SUM(amount) as sum").Where("app_id = ? AND preimage IS NOT NULL AND created_at > ?", appPermission.AppId, GetStartOfBudget(appPermission.BudgetRenewal, appPermission.App.CreatedAt)).Scan(&result) + svc.db.Table("payments").Select("SUM(amount) as sum").Where("app_id = ? AND preimage IS NOT NULL AND created_at > ?", appPermission.AppId, utils.GetStartOfBudget(appPermission.BudgetRenewal, appPermission.App.CreatedAt)).Scan(&result) return int64(result.Sum) } @@ -748,9 +755,9 @@ func (svc *Service) PublishNip47Info(ctx context.Context, relay *nostr.Relay) er ev.Kind = nip47.INFO_EVENT_KIND ev.Content = nip47.CAPABILITIES ev.CreatedAt = nostr.Now() - ev.PubKey = svc.cfg.NostrPublicKey + ev.PubKey = svc.cfg.GetNostrPublicKey() ev.Tags = nostr.Tags{[]string{"notifications", nip47.NOTIFICATION_TYPES}} - err := ev.Sign(svc.cfg.NostrSecretKey) + err := ev.Sign(svc.cfg.GetNostrSecretKey()) if err != nil { return err } @@ -761,8 +768,8 @@ func (svc *Service) PublishNip47Info(ctx context.Context, relay *nostr.Relay) er return nil } -func (svc *Service) LogFilePath() string { - return filepath.Join(svc.cfg.Env.Workdir, logDir, logFilename) +func (svc *Service) GetLogFilePath() string { + return filepath.Join(svc.cfg.GetEnv().Workdir, logDir, logFilename) } func finishRestoreNode(logger *logrus.Logger, workDir string) { @@ -803,3 +810,24 @@ func finishRestoreNode(logger *logrus.Logger, workDir string) { logger.WithField("restoreDir", restoreDir).Info("removed restore directory") } } + +func (svc *Service) StopDb() error { + db, err := svc.db.DB() + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + + err = db.Close() + if err != nil { + return fmt.Errorf("failed to close database connection: %w", err) + } + return nil +} + +func (svc *Service) GetConfig() config.Config { + return svc.cfg +} + +func (svc *Service) GetAlbyOAuthSvc() alby.AlbyOAuthService { + return svc.albyOAuthSvc +} diff --git a/service/models.go b/service/models.go new file mode 100644 index 00000000..59ecbf9d --- /dev/null +++ b/service/models.go @@ -0,0 +1,20 @@ +package service + +import ( + "github.com/getAlby/nostr-wallet-connect/alby" + "github.com/getAlby/nostr-wallet-connect/config" + "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/lnclient" +) + +type Service interface { + GetLNClient() lnclient.LNClient + GetConfig() config.Config + StartApp(encryptionKey string) error + StopApp() + StopLNClient() error + StopDb() error + GetBudgetUsage(appPermission *db.AppPermission) int64 + GetLogFilePath() string + GetAlbyOAuthSvc() alby.AlbyOAuthService +} diff --git a/service_test.go b/service_test.go index 9e970310..b0cb4aba 100644 --- a/service_test.go +++ b/service_test.go @@ -14,10 +14,11 @@ import ( "github.com/stretchr/testify/assert" "gorm.io/gorm" + "github.com/getAlby/nostr-wallet-connect/config" + "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/events" + "github.com/getAlby/nostr-wallet-connect/lnclient" "github.com/getAlby/nostr-wallet-connect/migrations" - "github.com/getAlby/nostr-wallet-connect/models/config" - "github.com/getAlby/nostr-wallet-connect/models/lnclient" "github.com/getAlby/nostr-wallet-connect/nip47" ) @@ -223,7 +224,7 @@ var mockNodeInfo = lnclient.NodeInfo{ var mockTime = time.Unix(1693876963, 0) var mockTimeUnix = mockTime.Unix() -var mockTransactions = []Nip47Transaction{ +var mockTransactions = []nip47.Transaction{ { Type: "incoming", Invoice: mockInvoice, @@ -287,7 +288,7 @@ func TestHasPermission_Expired(t *testing.T) { budgetRenewal := "never" expiresAt := time.Now().Add(-24 * time.Hour) - appPermission := &AppPermission{ + appPermission := &db.AppPermission{ AppId: app.ID, App: *app, RequestMethod: nip47.PAY_INVOICE_METHOD, @@ -316,7 +317,7 @@ func TestHasPermission_Exceeded(t *testing.T) { budgetRenewal := "never" expiresAt := time.Now().Add(24 * time.Hour) - appPermission := &AppPermission{ + appPermission := &db.AppPermission{ AppId: app.ID, App: *app, RequestMethod: nip47.PAY_INVOICE_METHOD, @@ -345,7 +346,7 @@ func TestHasPermission_OK(t *testing.T) { budgetRenewal := "never" expiresAt := time.Now().Add(24 * time.Hour) - appPermission := &AppPermission{ + appPermission := &db.AppPermission{ AppId: app.ID, App: *app, RequestMethod: nip47.PAY_INVOICE_METHOD, @@ -381,12 +382,12 @@ func TestCreateResponse(t *testing.T) { reqEvent.ID = "12345" - ss, err := nip04.ComputeSharedSecret(reqPubkey, svc.cfg.NostrSecretKey) + ss, err := nip04.ComputeSharedSecret(reqPubkey, svc.cfg.GetNostrSecretKey()) assert.NoError(t, err) - nip47Response := &Nip47Response{ + nip47Response := &nip47.Response{ ResultType: nip47.GET_BALANCE_METHOD, - Result: Nip47BalanceResponse{ + Result: nip47.BalanceResponse{ Balance: 1000, }, } @@ -394,18 +395,18 @@ func TestCreateResponse(t *testing.T) { assert.NoError(t, err) assert.Equal(t, reqPubkey, res.Tags.GetFirst([]string{"p"}).Value()) assert.Equal(t, reqEvent.ID, res.Tags.GetFirst([]string{"e"}).Value()) - assert.Equal(t, svc.cfg.NostrPublicKey, res.PubKey) + assert.Equal(t, svc.cfg.GetNostrPublicKey(), res.PubKey) decrypted, err := nip04.Decrypt(res.Content, ss) assert.NoError(t, err) - unmarshalledResponse := Nip47Response{ - Result: &Nip47BalanceResponse{}, + unmarshalledResponse := nip47.Response{ + Result: &nip47.BalanceResponse{}, } err = json.Unmarshal([]byte(decrypted), &unmarshalledResponse) assert.NoError(t, err) assert.Equal(t, nip47Response.ResultType, unmarshalledResponse.ResultType) - assert.Equal(t, nip47Response.Result, *unmarshalledResponse.Result.(*Nip47BalanceResponse)) + assert.Equal(t, nip47Response.Result, *unmarshalledResponse.Result.(*nip47.BalanceResponse)) } func TestHandleEncryption(t *testing.T) {} @@ -420,7 +421,7 @@ func TestHandleMultiPayInvoiceEvent(t *testing.T) { app, ss, err := createApp(svc) assert.NoError(t, err) - request := &Nip47Request{} + request := &nip47.Request{} err = json.Unmarshal([]byte(nip47MultiPayJson), request) assert.NoError(t, err) @@ -432,17 +433,17 @@ func TestHandleMultiPayInvoiceEvent(t *testing.T) { PubKey: app.NostrPubkey, Content: payload, } - requestEvent := &RequestEvent{ + requestEvent := &db.RequestEvent{ Content: reqEvent.Content, } reqEvent.ID = "multi_pay_invoice_without_permission" requestEvent.NostrId = reqEvent.ID - responses := []*Nip47Response{} + responses := []*nip47.Response{} dTags := []nostr.Tags{} - publishResponse := func(response *Nip47Response, tags nostr.Tags) { + publishResponse := func(response *nip47.Response, tags nostr.Tags) { responses = append(responses, response) dTags = append(dTags, tags) } @@ -460,7 +461,7 @@ func TestHandleMultiPayInvoiceEvent(t *testing.T) { maxAmount := 1000 budgetRenewal := "never" expiresAt := time.Now().Add(24 * time.Hour) - appPermission := &AppPermission{ + appPermission := &db.AppPermission{ AppId: app.ID, App: *app, RequestMethod: nip47.PAY_INVOICE_METHOD, @@ -473,13 +474,13 @@ func TestHandleMultiPayInvoiceEvent(t *testing.T) { reqEvent.ID = "multi_pay_invoice_with_permission" requestEvent.NostrId = reqEvent.ID - responses = []*Nip47Response{} + responses = []*nip47.Response{} dTags = []nostr.Tags{} svc.HandleMultiPayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, 2, len(responses)) for i := 0; i < len(responses); i++ { - assert.Equal(t, responses[i].Result.(Nip47PayResponse).Preimage, "123preimage") + assert.Equal(t, responses[i].Result.(nip47.PayResponse).Preimage, "123preimage") assert.Equal(t, mockPaymentHash, dTags[i].GetFirst([]string{"d"}).Value()) } @@ -493,7 +494,7 @@ func TestHandleMultiPayInvoiceEvent(t *testing.T) { reqEvent.ID = "multi_pay_invoice_with_one_malformed_invoice" requestEvent.NostrId = reqEvent.ID - responses = []*Nip47Response{} + responses = []*nip47.Response{} dTags = []nostr.Tags{} svc.HandleMultiPayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) @@ -502,13 +503,13 @@ func TestHandleMultiPayInvoiceEvent(t *testing.T) { assert.Equal(t, responses[0].Error.Code, nip47.ERROR_INTERNAL) assert.Equal(t, mockPaymentHash, dTags[1].GetFirst([]string{"d"}).Value()) - assert.Equal(t, responses[1].Result.(Nip47PayResponse).Preimage, "123preimage") + assert.Equal(t, responses[1].Result.(nip47.PayResponse).Preimage, "123preimage") // we've spent 369 till here in three payments // budget overflow newMaxAmount := 500 - err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("max_amount", newMaxAmount).Error + err = svc.db.Model(&db.AppPermission{}).Where("app_id = ?", app.ID).Update("max_amount", newMaxAmount).Error assert.NoError(t, err) err = json.Unmarshal([]byte(nip47MultiPayOneOverflowingBudgetJson), request) @@ -520,7 +521,7 @@ func TestHandleMultiPayInvoiceEvent(t *testing.T) { reqEvent.ID = "multi_pay_invoice_with_budget_overflow" requestEvent.NostrId = reqEvent.ID - responses = []*Nip47Response{} + responses = []*nip47.Response{} dTags = []nostr.Tags{} svc.HandleMultiPayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) @@ -529,7 +530,7 @@ func TestHandleMultiPayInvoiceEvent(t *testing.T) { // publishResponse as it's called earlier assert.Equal(t, responses[0].Error.Code, nip47.ERROR_QUOTA_EXCEEDED) assert.Equal(t, mockPaymentHash500, dTags[0].GetFirst([]string{"d"}).Value()) - assert.Equal(t, responses[1].Result.(Nip47PayResponse).Preimage, "123preimage") + assert.Equal(t, responses[1].Result.(nip47.PayResponse).Preimage, "123preimage") assert.Equal(t, mockPaymentHash, dTags[1].GetFirst([]string{"d"}).Value()) } @@ -544,7 +545,7 @@ func TestHandleMultiPayKeysendEvent(t *testing.T) { app, ss, err := createApp(svc) assert.NoError(t, err) - request := &Nip47Request{} + request := &nip47.Request{} err = json.Unmarshal([]byte(nip47MultiPayKeysendJson), request) assert.NoError(t, err) @@ -556,17 +557,17 @@ func TestHandleMultiPayKeysendEvent(t *testing.T) { PubKey: app.NostrPubkey, Content: payload, } - requestEvent := &RequestEvent{ + requestEvent := &db.RequestEvent{ Content: reqEvent.Content, } reqEvent.ID = "multi_pay_keysend_without_permission" requestEvent.NostrId = reqEvent.ID - responses := []*Nip47Response{} + responses := []*nip47.Response{} dTags := []nostr.Tags{} - publishResponse := func(response *Nip47Response, tags nostr.Tags) { + publishResponse := func(response *nip47.Response, tags nostr.Tags) { responses = append(responses, response) dTags = append(dTags, tags) } @@ -585,7 +586,7 @@ func TestHandleMultiPayKeysendEvent(t *testing.T) { // because we need the same permission for keysend although // it works even with nip47.PAY_KEYSEND_METHOD, see // https://github.com/getAlby/nostr-wallet-connect/issues/189 - appPermission := &AppPermission{ + appPermission := &db.AppPermission{ AppId: app.ID, App: *app, RequestMethod: nip47.PAY_INVOICE_METHOD, @@ -598,13 +599,13 @@ func TestHandleMultiPayKeysendEvent(t *testing.T) { reqEvent.ID = "multi_pay_keysend_with_permission" requestEvent.NostrId = reqEvent.ID - responses = []*Nip47Response{} + responses = []*nip47.Response{} dTags = []nostr.Tags{} svc.HandleMultiPayKeysendEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, 2, len(responses)) for i := 0; i < len(responses); i++ { - assert.Equal(t, responses[i].Result.(Nip47PayResponse).Preimage, "12345preimage") + assert.Equal(t, responses[i].Result.(nip47.PayResponse).Preimage, "12345preimage") assert.Equal(t, "123pubkey", dTags[i].GetFirst([]string{"d"}).Value()) } @@ -612,7 +613,7 @@ func TestHandleMultiPayKeysendEvent(t *testing.T) { // budget overflow newMaxAmount := 500 - err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("max_amount", newMaxAmount).Error + err = svc.db.Model(&db.AppPermission{}).Where("app_id = ?", app.ID).Update("max_amount", newMaxAmount).Error assert.NoError(t, err) err = json.Unmarshal([]byte(nip47MultiPayKeysendOneOverflowingBudgetJson), request) @@ -624,13 +625,13 @@ func TestHandleMultiPayKeysendEvent(t *testing.T) { reqEvent.ID = "multi_pay_keysend_with_budget_overflow" requestEvent.NostrId = reqEvent.ID - responses = []*Nip47Response{} + responses = []*nip47.Response{} dTags = []nostr.Tags{} svc.HandleMultiPayKeysendEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, responses[0].Error.Code, nip47.ERROR_QUOTA_EXCEEDED) assert.Equal(t, "500pubkey", dTags[0].GetFirst([]string{"d"}).Value()) - assert.Equal(t, responses[1].Result.(Nip47PayResponse).Preimage, "12345preimage") + assert.Equal(t, responses[1].Result.(nip47.PayResponse).Preimage, "12345preimage") assert.Equal(t, "customId", dTags[1].GetFirst([]string{"d"}).Value()) } @@ -644,7 +645,7 @@ func TestHandleGetBalanceEvent(t *testing.T) { app, ss, err := createApp(svc) assert.NoError(t, err) - request := &Nip47Request{} + request := &nip47.Request{} err = json.Unmarshal([]byte(nip47GetBalanceJson), request) assert.NoError(t, err) @@ -656,16 +657,16 @@ func TestHandleGetBalanceEvent(t *testing.T) { PubKey: app.NostrPubkey, Content: payload, } - requestEvent := &RequestEvent{ + requestEvent := &db.RequestEvent{ Content: reqEvent.Content, } reqEvent.ID = "test_get_balance_without_permission" requestEvent.NostrId = reqEvent.ID - responses := []*Nip47Response{} + responses := []*nip47.Response{} - publishResponse := func(response *Nip47Response, tags nostr.Tags) { + publishResponse := func(response *nip47.Response, tags nostr.Tags) { responses = append(responses, response) } @@ -678,7 +679,7 @@ func TestHandleGetBalanceEvent(t *testing.T) { // with permission expiresAt := time.Now().Add(24 * time.Hour) - appPermission := &AppPermission{ + appPermission := &db.AppPermission{ AppId: app.ID, App: *app, RequestMethod: nip47.GET_BALANCE_METHOD, @@ -689,15 +690,15 @@ func TestHandleGetBalanceEvent(t *testing.T) { reqEvent.ID = "test_get_balance_with_permission" requestEvent.NostrId = reqEvent.ID - responses = []*Nip47Response{} + responses = []*nip47.Response{} svc.HandleGetBalanceEvent(ctx, request, requestEvent, app, publishResponse) - assert.Equal(t, responses[0].Result.(*Nip47BalanceResponse).Balance, int64(21000)) + assert.Equal(t, responses[0].Result.(*nip47.BalanceResponse).Balance, int64(21000)) // create pay_invoice permission maxAmount := 1000 budgetRenewal := "never" - appPermission = &AppPermission{ + appPermission = &db.AppPermission{ AppId: app.ID, App: *app, RequestMethod: nip47.PAY_INVOICE_METHOD, @@ -709,12 +710,12 @@ func TestHandleGetBalanceEvent(t *testing.T) { assert.NoError(t, err) reqEvent.ID = "test_get_balance_with_budget" - responses = []*Nip47Response{} + responses = []*nip47.Response{} svc.HandleGetBalanceEvent(ctx, request, requestEvent, app, publishResponse) - assert.Equal(t, int64(21000), responses[0].Result.(*Nip47BalanceResponse).Balance) - assert.Equal(t, 1000000, responses[0].Result.(*Nip47BalanceResponse).MaxAmount) - assert.Equal(t, "never", responses[0].Result.(*Nip47BalanceResponse).BudgetRenewal) + assert.Equal(t, int64(21000), responses[0].Result.(*nip47.BalanceResponse).Balance) + assert.Equal(t, 1000000, responses[0].Result.(*nip47.BalanceResponse).MaxAmount) + assert.Equal(t, "never", responses[0].Result.(*nip47.BalanceResponse).BudgetRenewal) } func TestHandlePayInvoiceEvent(t *testing.T) { @@ -727,7 +728,7 @@ func TestHandlePayInvoiceEvent(t *testing.T) { app, ss, err := createApp(svc) assert.NoError(t, err) - request := &Nip47Request{} + request := &nip47.Request{} err = json.Unmarshal([]byte(nip47PayJson), request) assert.NoError(t, err) @@ -739,16 +740,16 @@ func TestHandlePayInvoiceEvent(t *testing.T) { PubKey: app.NostrPubkey, Content: payload, } - requestEvent := &RequestEvent{ + requestEvent := &db.RequestEvent{ Content: reqEvent.Content, } reqEvent.ID = "pay_invoice_without_permission" requestEvent.NostrId = reqEvent.ID - responses := []*Nip47Response{} + responses := []*nip47.Response{} - publishResponse := func(response *Nip47Response, tags nostr.Tags) { + publishResponse := func(response *nip47.Response, tags nostr.Tags) { responses = append(responses, response) } @@ -760,7 +761,7 @@ func TestHandlePayInvoiceEvent(t *testing.T) { maxAmount := 1000 budgetRenewal := "never" expiresAt := time.Now().Add(24 * time.Hour) - appPermission := &AppPermission{ + appPermission := &db.AppPermission{ AppId: app.ID, App: *app, RequestMethod: nip47.PAY_INVOICE_METHOD, @@ -773,10 +774,10 @@ func TestHandlePayInvoiceEvent(t *testing.T) { reqEvent.ID = "pay_invoice_with_permission" requestEvent.NostrId = reqEvent.ID - responses = []*Nip47Response{} + responses = []*nip47.Response{} svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - assert.Equal(t, responses[0].Result.(Nip47PayResponse).Preimage, "123preimage") + assert.Equal(t, responses[0].Result.(nip47.PayResponse).Preimage, "123preimage") // malformed invoice err = json.Unmarshal([]byte(nip47PayJsonNoInvoice), request) @@ -788,7 +789,7 @@ func TestHandlePayInvoiceEvent(t *testing.T) { reqEvent.ID = "pay_invoice_with_malformed_invoice" requestEvent.NostrId = reqEvent.ID - responses = []*Nip47Response{} + responses = []*nip47.Response{} svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, nip47.ERROR_INTERNAL, responses[0].Error.Code) @@ -803,14 +804,14 @@ func TestHandlePayInvoiceEvent(t *testing.T) { reqEvent.ID = "pay_invoice_with_wrong_request_method" requestEvent.NostrId = reqEvent.ID - responses = []*Nip47Response{} + responses = []*nip47.Response{} svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, nip47.ERROR_RESTRICTED, responses[0].Error.Code) // budget overflow newMaxAmount := 100 - err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("max_amount", newMaxAmount).Error + err = svc.db.Model(&db.AppPermission{}).Where("app_id = ?", app.ID).Update("max_amount", newMaxAmount).Error assert.NoError(t, err) err = json.Unmarshal([]byte(nip47PayJson), request) @@ -822,33 +823,33 @@ func TestHandlePayInvoiceEvent(t *testing.T) { reqEvent.ID = "pay_invoice_with_budget_overflow" requestEvent.NostrId = reqEvent.ID - responses = []*Nip47Response{} + responses = []*nip47.Response{} svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, nip47.ERROR_QUOTA_EXCEEDED, responses[0].Error.Code) // budget expiry newExpiry := time.Now().Add(-24 * time.Hour) - err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("max_amount", maxAmount).Update("expires_at", newExpiry).Error + err = svc.db.Model(&db.AppPermission{}).Where("app_id = ?", app.ID).Update("max_amount", maxAmount).Update("expires_at", newExpiry).Error assert.NoError(t, err) reqEvent.ID = "pay_invoice_with_budget_expiry" requestEvent.NostrId = reqEvent.ID - responses = []*Nip47Response{} + responses = []*nip47.Response{} svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, nip47.ERROR_EXPIRED, responses[0].Error.Code) // check again - err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("expires_at", nil).Error + err = svc.db.Model(&db.AppPermission{}).Where("app_id = ?", app.ID).Update("expires_at", nil).Error assert.NoError(t, err) reqEvent.ID = "pay_invoice_after_change" requestEvent.NostrId = reqEvent.ID - responses = []*Nip47Response{} + responses = []*nip47.Response{} svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - assert.Equal(t, responses[0].Result.(Nip47PayResponse).Preimage, "123preimage") + assert.Equal(t, responses[0].Result.(nip47.PayResponse).Preimage, "123preimage") } func TestHandlePayKeysendEvent(t *testing.T) { @@ -861,7 +862,7 @@ func TestHandlePayKeysendEvent(t *testing.T) { app, ss, err := createApp(svc) assert.NoError(t, err) - request := &Nip47Request{} + request := &nip47.Request{} err = json.Unmarshal([]byte(nip47KeysendJson), request) assert.NoError(t, err) @@ -873,16 +874,16 @@ func TestHandlePayKeysendEvent(t *testing.T) { PubKey: app.NostrPubkey, Content: payload, } - requestEvent := &RequestEvent{ + requestEvent := &db.RequestEvent{ Content: reqEvent.Content, } reqEvent.ID = "pay_keysend_without_permission" requestEvent.NostrId = reqEvent.ID - responses := []*Nip47Response{} + responses := []*nip47.Response{} - publishResponse := func(response *Nip47Response, tags nostr.Tags) { + publishResponse := func(response *nip47.Response, tags nostr.Tags) { responses = append(responses, response) } @@ -897,7 +898,7 @@ func TestHandlePayKeysendEvent(t *testing.T) { // because we need the same permission for keysend although // it works even with nip47.PAY_KEYSEND_METHOD, see // https://github.com/getAlby/nostr-wallet-connect/issues/189 - appPermission := &AppPermission{ + appPermission := &db.AppPermission{ AppId: app.ID, App: *app, RequestMethod: nip47.PAY_INVOICE_METHOD, @@ -910,14 +911,14 @@ func TestHandlePayKeysendEvent(t *testing.T) { reqEvent.ID = "pay_keysend_with_permission" requestEvent.NostrId = reqEvent.ID - responses = []*Nip47Response{} + responses = []*nip47.Response{} svc.HandlePayKeysendEvent(ctx, request, requestEvent, app, publishResponse) - assert.Equal(t, responses[0].Result.(Nip47PayResponse).Preimage, "12345preimage") + assert.Equal(t, responses[0].Result.(nip47.PayResponse).Preimage, "12345preimage") // budget overflow newMaxAmount := 100 - err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("max_amount", newMaxAmount).Error + err = svc.db.Model(&db.AppPermission{}).Where("app_id = ?", app.ID).Update("max_amount", newMaxAmount).Error assert.NoError(t, err) err = json.Unmarshal([]byte(nip47KeysendJson), request) @@ -929,7 +930,7 @@ func TestHandlePayKeysendEvent(t *testing.T) { reqEvent.ID = "pay_keysend_with_budget_overflow" requestEvent.NostrId = reqEvent.ID - responses = []*Nip47Response{} + responses = []*nip47.Response{} svc.HandlePayKeysendEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, nip47.ERROR_QUOTA_EXCEEDED, responses[0].Error.Code) @@ -945,7 +946,7 @@ func TestHandleLookupInvoiceEvent(t *testing.T) { app, ss, err := createApp(svc) assert.NoError(t, err) - request := &Nip47Request{} + request := &nip47.Request{} err = json.Unmarshal([]byte(nip47LookupInvoiceJson), request) assert.NoError(t, err) @@ -957,16 +958,16 @@ func TestHandleLookupInvoiceEvent(t *testing.T) { PubKey: app.NostrPubkey, Content: payload, } - requestEvent := &RequestEvent{ + requestEvent := &db.RequestEvent{ Content: reqEvent.Content, } reqEvent.ID = "test_lookup_invoice_without_permission" requestEvent.NostrId = reqEvent.ID - responses := []*Nip47Response{} + responses := []*nip47.Response{} - publishResponse := func(response *Nip47Response, tags nostr.Tags) { + publishResponse := func(response *nip47.Response, tags nostr.Tags) { responses = append(responses, response) } @@ -976,7 +977,7 @@ func TestHandleLookupInvoiceEvent(t *testing.T) { // with permission expiresAt := time.Now().Add(24 * time.Hour) - appPermission := &AppPermission{ + appPermission := &db.AppPermission{ AppId: app.ID, App: *app, RequestMethod: nip47.LOOKUP_INVOICE_METHOD, @@ -987,10 +988,10 @@ func TestHandleLookupInvoiceEvent(t *testing.T) { reqEvent.ID = "test_lookup_invoice_with_permission" requestEvent.NostrId = reqEvent.ID - responses = []*Nip47Response{} + responses = []*nip47.Response{} svc.HandleLookupInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - transaction := responses[0].Result.(*Nip47LookupInvoiceResponse) + transaction := responses[0].Result.(*nip47.LookupInvoiceResponse) assert.Equal(t, mockTransaction.Type, transaction.Type) assert.Equal(t, mockTransaction.Invoice, transaction.Invoice) assert.Equal(t, mockTransaction.Description, transaction.Description) @@ -1012,7 +1013,7 @@ func TestHandleMakeInvoiceEvent(t *testing.T) { app, ss, err := createApp(svc) assert.NoError(t, err) - request := &Nip47Request{} + request := &nip47.Request{} err = json.Unmarshal([]byte(nip47MakeInvoiceJson), request) assert.NoError(t, err) @@ -1024,16 +1025,16 @@ func TestHandleMakeInvoiceEvent(t *testing.T) { PubKey: app.NostrPubkey, Content: payload, } - requestEvent := &RequestEvent{ + requestEvent := &db.RequestEvent{ Content: reqEvent.Content, } reqEvent.ID = "test_make_invoice_without_permission" requestEvent.NostrId = reqEvent.ID - responses := []*Nip47Response{} + responses := []*nip47.Response{} - publishResponse := func(response *Nip47Response, tags nostr.Tags) { + publishResponse := func(response *nip47.Response, tags nostr.Tags) { responses = append(responses, response) } @@ -1043,7 +1044,7 @@ func TestHandleMakeInvoiceEvent(t *testing.T) { // with permission expiresAt := time.Now().Add(24 * time.Hour) - appPermission := &AppPermission{ + appPermission := &db.AppPermission{ AppId: app.ID, App: *app, RequestMethod: nip47.MAKE_INVOICE_METHOD, @@ -1054,10 +1055,10 @@ func TestHandleMakeInvoiceEvent(t *testing.T) { reqEvent.ID = "test_make_invoice_with_permission" requestEvent.NostrId = reqEvent.ID - responses = []*Nip47Response{} + responses = []*nip47.Response{} svc.HandleMakeInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - assert.Equal(t, mockTransaction.Preimage, responses[0].Result.(*Nip47MakeInvoiceResponse).Preimage) + assert.Equal(t, mockTransaction.Preimage, responses[0].Result.(*nip47.MakeInvoiceResponse).Preimage) } func TestHandleListTransactionsEvent(t *testing.T) { @@ -1070,7 +1071,7 @@ func TestHandleListTransactionsEvent(t *testing.T) { app, ss, err := createApp(svc) assert.NoError(t, err) - request := &Nip47Request{} + request := &nip47.Request{} err = json.Unmarshal([]byte(nip47ListTransactionsJson), request) assert.NoError(t, err) @@ -1082,16 +1083,16 @@ func TestHandleListTransactionsEvent(t *testing.T) { PubKey: app.NostrPubkey, Content: payload, } - requestEvent := &RequestEvent{ + requestEvent := &db.RequestEvent{ Content: reqEvent.Content, } reqEvent.ID = "test_list_transactions_without_permission" requestEvent.NostrId = reqEvent.ID - responses := []*Nip47Response{} + responses := []*nip47.Response{} - publishResponse := func(response *Nip47Response, tags nostr.Tags) { + publishResponse := func(response *nip47.Response, tags nostr.Tags) { responses = append(responses, response) } @@ -1101,7 +1102,7 @@ func TestHandleListTransactionsEvent(t *testing.T) { // with permission expiresAt := time.Now().Add(24 * time.Hour) - appPermission := &AppPermission{ + appPermission := &db.AppPermission{ AppId: app.ID, App: *app, RequestMethod: nip47.LIST_TRANSACTIONS_METHOD, @@ -1112,11 +1113,11 @@ func TestHandleListTransactionsEvent(t *testing.T) { reqEvent.ID = "test_list_transactions_with_permission" requestEvent.NostrId = reqEvent.ID - responses = []*Nip47Response{} + responses = []*nip47.Response{} svc.HandleListTransactionsEvent(ctx, request, requestEvent, app, publishResponse) - assert.Equal(t, 2, len(responses[0].Result.(*Nip47ListTransactionsResponse).Transactions)) - transaction := responses[0].Result.(*Nip47ListTransactionsResponse).Transactions[0] + assert.Equal(t, 2, len(responses[0].Result.(*nip47.ListTransactionsResponse).Transactions)) + transaction := responses[0].Result.(*nip47.ListTransactionsResponse).Transactions[0] assert.Equal(t, mockTransactions[0].Type, transaction.Type) assert.Equal(t, mockTransactions[0].Invoice, transaction.Invoice) assert.Equal(t, mockTransactions[0].Description, transaction.Description) @@ -1138,7 +1139,7 @@ func TestHandleGetInfoEvent(t *testing.T) { app, ss, err := createApp(svc) assert.NoError(t, err) - request := &Nip47Request{} + request := &nip47.Request{} err = json.Unmarshal([]byte(nip47GetInfoJson), request) assert.NoError(t, err) @@ -1150,16 +1151,16 @@ func TestHandleGetInfoEvent(t *testing.T) { PubKey: app.NostrPubkey, Content: payload, } - requestEvent := &RequestEvent{ + requestEvent := &db.RequestEvent{ Content: reqEvent.Content, } reqEvent.ID = "test_get_info_without_permission" requestEvent.NostrId = reqEvent.ID - responses := []*Nip47Response{} + responses := []*nip47.Response{} - publishResponse := func(response *Nip47Response, tags nostr.Tags) { + publishResponse := func(response *nip47.Response, tags nostr.Tags) { responses = append(responses, response) } @@ -1168,7 +1169,7 @@ func TestHandleGetInfoEvent(t *testing.T) { assert.Equal(t, nip47.ERROR_RESTRICTED, responses[0].Error.Code) expiresAt := time.Now().Add(24 * time.Hour) - appPermission := &AppPermission{ + appPermission := &db.AppPermission{ AppId: app.ID, App: *app, RequestMethod: nip47.GET_INFO_METHOD, @@ -1179,10 +1180,10 @@ func TestHandleGetInfoEvent(t *testing.T) { reqEvent.ID = "test_get_info_with_permission" requestEvent.NostrId = reqEvent.ID - responses = []*Nip47Response{} + responses = []*nip47.Response{} svc.HandleGetInfoEvent(ctx, request, requestEvent, app, publishResponse) - nodeInfo := responses[0].Result.(*Nip47GetInfoResponse) + nodeInfo := responses[0].Result.(*nip47.GetInfoResponse) assert.Equal(t, mockNodeInfo.Alias, nodeInfo.Alias) assert.Equal(t, mockNodeInfo.Color, nodeInfo.Color) assert.Equal(t, mockNodeInfo.Pubkey, nodeInfo.Pubkey) @@ -1203,44 +1204,45 @@ func createTestService(ln *MockLn) (svc *Service, err error) { logger.SetOutput(os.Stdout) logger.SetLevel(logrus.InfoLevel) - err = migrations.Migrate(gormDb, &config.AppConfig{ + appConfig := &config.AppConfig{ Workdir: ".test", - }, logger) - if err != nil { - return nil, err } - sk := nostr.GeneratePrivateKey() - pk, err := nostr.GetPublicKey(sk) + + err = migrations.Migrate(gormDb, appConfig, logger) if err != nil { return nil, err } + cfg := config.NewConfig( + gormDb, + appConfig, + logger, + ) + + cfg.Start("") + return &Service{ - cfg: &Config{ - db: gormDb, - NostrSecretKey: sk, - NostrPublicKey: pk, - }, + cfg: cfg, db: gormDb, lnClient: ln, - Logger: logger, - EventPublisher: events.NewEventPublisher(logger), + logger: logger, + eventPublisher: events.NewEventPublisher(logger), }, nil } -func createApp(svc *Service) (app *App, ss []byte, err error) { +func createApp(svc *Service) (app *db.App, ss []byte, err error) { senderPrivkey := nostr.GeneratePrivateKey() senderPubkey, err := nostr.GetPublicKey(senderPrivkey) if err != nil { return nil, nil, err } - ss, err = nip04.ComputeSharedSecret(svc.cfg.NostrPublicKey, senderPrivkey) + ss, err = nip04.ComputeSharedSecret(svc.cfg.GetNostrPublicKey(), senderPrivkey) if err != nil { return nil, nil, err } - app = &App{Name: "test", NostrPubkey: senderPubkey} + app = &db.App{Name: "test", NostrPubkey: senderPubkey} err = svc.db.Create(app).Error if err != nil { return nil, nil, err @@ -1256,8 +1258,8 @@ func NewMockLn() (*MockLn, error) { return &MockLn{}, nil } -func (mln *MockLn) SendPaymentSync(ctx context.Context, payReq string) (*lnclient.Nip47PayInvoiceResponse, error) { - return &lnclient.Nip47PayInvoiceResponse{ +func (mln *MockLn) SendPaymentSync(ctx context.Context, payReq string) (*lnclient.PayInvoiceResponse, error) { + return &lnclient.PayInvoiceResponse{ Preimage: "123preimage", }, nil } @@ -1274,15 +1276,15 @@ func (mln *MockLn) GetInfo(ctx context.Context) (info *lnclient.NodeInfo, err er return &mockNodeInfo, nil } -func (mln *MockLn) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *Nip47Transaction, err error) { +func (mln *MockLn) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *nip47.Transaction, err error) { return mockTransaction, nil } -func (mln *MockLn) LookupInvoice(ctx context.Context, paymentHash string) (transaction *Nip47Transaction, err error) { +func (mln *MockLn) LookupInvoice(ctx context.Context, paymentHash string) (transaction *nip47.Transaction, err error) { return mockTransaction, nil } -func (mln *MockLn) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (invoices []Nip47Transaction, err error) { +func (mln *MockLn) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (invoices []nip47.Transaction, err error) { return mockTransactions, nil } func (mln *MockLn) Shutdown() error { @@ -1343,3 +1345,4 @@ func (mln *MockLn) GetNodeStatus(ctx context.Context) (nodeStatus *lnclient.Node func (mln *MockLn) GetNetworkGraph(nodeIds []string) (lnclient.NetworkGraphResponse, error) { return nil, nil } +func (mln *MockLn) UpdateLastWalletSyncRequest() {} diff --git a/start.go b/start.go index 1c78a16f..2e44ac97 100644 --- a/start.go +++ b/start.go @@ -7,31 +7,28 @@ import ( "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" + "github.com/sirupsen/logrus" "github.com/getAlby/nostr-wallet-connect/events" ) func (svc *Service) StartNostr(ctx context.Context, encryptionKey string) error { - relayUrl, _ := svc.cfg.Get("Relay", encryptionKey) - nostrSecretKey, _ := svc.cfg.Get("NostrSecretKey", encryptionKey) - if nostrSecretKey == "" { - nostrSecretKey = nostr.GeneratePrivateKey() - svc.cfg.SetUpdate("NostrSecretKey", nostrSecretKey, encryptionKey) - } - nostrPublicKey, err := nostr.GetPublicKey(nostrSecretKey) - if err != nil { - svc.Logger.Errorf("Error converting nostr privkey to pubkey: %v", err) - return err - } - svc.cfg.NostrSecretKey = nostrSecretKey - svc.cfg.NostrPublicKey = nostrPublicKey + relayUrl := svc.cfg.GetRelayUrl() - npub, err := nip19.EncodePublicKey(svc.cfg.NostrPublicKey) + err := svc.cfg.Start(encryptionKey) if err != nil { - svc.Logger.Fatalf("Error converting nostr privkey to pubkey: %v", err) + svc.logger.WithError(err).Fatal("Failed to start config") } - svc.Logger.Infof("Starting nostr-wallet-connect. npub: %s hex: %s", npub, svc.cfg.NostrPublicKey) + npub, err := nip19.EncodePublicKey(svc.cfg.GetNostrPublicKey()) + if err != nil { + svc.logger.WithError(err).Fatal("Error converting nostr privkey to pubkey") + } + + svc.logger.WithFields(logrus.Fields{ + "npub": npub, + "hex": svc.cfg.GetNostrPublicKey(), + }).Info("Starting nostr-wallet-connect") svc.wg.Add(1) go func() { //Start infinite loop which will be only broken by canceling ctx (SIGINT) @@ -43,11 +40,11 @@ func (svc *Service) StartNostr(ctx context.Context, encryptionKey string) error if i > 0 { sleepDuration := 10 contextCancelled := false - svc.Logger.Infof("[Iteration %d] Retrying in %d seconds...", i, sleepDuration) + svc.logger.Infof("[Iteration %d] Retrying in %d seconds...", i, sleepDuration) select { case <-ctx.Done(): //context cancelled - svc.Logger.Info("service context cancelled while waiting for retry") + svc.logger.Info("service context cancelled while waiting for retry") contextCancelled = true case <-time.After(time.Duration(sleepDuration) * time.Second): //timeout } @@ -58,49 +55,49 @@ func (svc *Service) StartNostr(ctx context.Context, encryptionKey string) error if relay != nil && relay.IsConnected() { err := relay.Close() if err != nil { - svc.Logger.WithError(err).Error("Could not close relay connection") + svc.logger.WithError(err).Error("Could not close relay connection") } } //connect to the relay - svc.Logger.Infof("Connecting to the relay: %s", relayUrl) + svc.logger.Infof("Connecting to the relay: %s", relayUrl) relay, err := nostr.RelayConnect(ctx, relayUrl, nostr.WithNoticeHandler(svc.noticeHandler)) if err != nil { - svc.Logger.WithError(err).Error("Failed to connect to relay") + svc.logger.WithError(err).Error("Failed to connect to relay") continue } //publish event with NIP-47 info err = svc.PublishNip47Info(ctx, relay) if err != nil { - svc.Logger.WithError(err).Error("Could not publish NIP47 info") + svc.logger.WithError(err).Error("Could not publish NIP47 info") } - svc.Logger.Info("Subscribing to events") - sub, err := relay.Subscribe(ctx, svc.createFilters(svc.cfg.NostrPublicKey)) + svc.logger.Info("Subscribing to events") + sub, err := relay.Subscribe(ctx, svc.createFilters(svc.cfg.GetNostrPublicKey())) if err != nil { - svc.Logger.WithError(err).Error("Failed to subscribe to events") + svc.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. - svc.Logger.WithError(err).Error("Got an error from the relay while listening to subscription.") + svc.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 } - svc.Logger.Info("Disconnecting from relay...") + svc.logger.Info("Disconnecting from relay...") if relay != nil && relay.IsConnected() { err := relay.Close() if err != nil { - svc.Logger.WithError(err).Error("Could not close relay connection") + svc.logger.WithError(err).Error("Could not close relay connection") } } svc.Shutdown() - svc.Logger.Info("Relay subroutine ended") + svc.logger.Info("Relay subroutine ended") svc.wg.Done() }() return nil @@ -108,7 +105,7 @@ func (svc *Service) StartNostr(ctx context.Context, encryptionKey string) error func (svc *Service) StartApp(encryptionKey string) error { if !svc.cfg.CheckUnlockPassword(encryptionKey) { - svc.Logger.Errorf("Invalid password") + svc.logger.Errorf("Invalid password") return errors.New("invalid password") } @@ -116,8 +113,8 @@ func (svc *Service) StartApp(encryptionKey string) error { err := svc.launchLNBackend(ctx, encryptionKey) if err != nil { - svc.Logger.Errorf("Failed to launch LN backend: %v", err) - svc.EventPublisher.Publish(&events.Event{ + svc.logger.Errorf("Failed to launch LN backend: %v", err) + svc.eventPublisher.Publish(&events.Event{ Event: "nwc_node_start_failed", }) cancelFn() @@ -129,6 +126,7 @@ func (svc *Service) StartApp(encryptionKey string) error { return nil } +// TODO: remove and call StopLNClient() instead func (svc *Service) StopApp() { if svc.appCancelFn != nil { svc.appCancelFn() @@ -138,7 +136,7 @@ func (svc *Service) StopApp() { func (svc *Service) Shutdown() { svc.StopLNClient() - svc.EventPublisher.Publish(&events.Event{ + svc.eventPublisher.Publish(&events.Event{ Event: "nwc_stopped", }) // wait for any remaining events diff --git a/utils.go b/utils/utils.go similarity index 99% rename from utils.go rename to utils/utils.go index 9c766d57..edbceefb 100644 --- a/utils.go +++ b/utils/utils.go @@ -1,4 +1,4 @@ -package main +package utils import ( "fmt" diff --git a/wails_app.go b/wails_app.go index 9734add0..bfd1d473 100644 --- a/wails_app.go +++ b/wails_app.go @@ -5,6 +5,7 @@ import ( "embed" "log" + "github.com/getAlby/nostr-wallet-connect/api" "github.com/sirupsen/logrus" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" @@ -17,13 +18,13 @@ var assets embed.FS type WailsApp struct { ctx context.Context svc *Service - api *API + api api.API } func NewApp(svc *Service) *WailsApp { return &WailsApp{ svc: svc, - api: NewAPI(svc), + api: api.NewAPI(svc, svc.logger, svc.db), } } @@ -34,7 +35,7 @@ func (app *WailsApp) startup(ctx context.Context) { } func LaunchWailsApp(app *WailsApp) { - logger := NewWailsLogger(app.svc.Logger) + logger := NewWailsLogger(app.svc.logger) err := wails.Run(&options.App{ Title: "Nostr Wallet Connect", diff --git a/wails_handlers.go b/wails_handlers.go index 1eb3052c..859dbdc5 100644 --- a/wails_handlers.go +++ b/wails_handlers.go @@ -9,7 +9,10 @@ import ( "github.com/sirupsen/logrus" - "github.com/getAlby/nostr-wallet-connect/models/api" + "github.com/getAlby/nostr-wallet-connect/alby" + "github.com/getAlby/nostr-wallet-connect/api" + "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/lsp" "github.com/wailsapp/wails/v2/pkg/runtime" ) @@ -33,9 +36,9 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string case len(authCodeMatch) > 1: code := authCodeMatch[1] - err := app.svc.AlbyOAuthSvc.CallbackHandler(ctx, code) + err := app.svc.albyOAuthSvc.CallbackHandler(ctx, code) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -55,7 +58,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string case len(appMatch) > 1: pubkey := appMatch[1] - userApp := App{} + userApp := db.App{} findResult := app.svc.db.Where("nostr_pubkey = ?", pubkey).First(&userApp) if findResult.RowsAffected == 0 { @@ -70,7 +73,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string updateAppRequest := &api.UpdateAppRequest{} err := json.Unmarshal([]byte(body), updateAppRequest) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -146,9 +149,9 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string switch route { case "/api/alby/me": - me, err := app.svc.AlbyOAuthSvc.GetMe(ctx) + me, err := app.svc.albyOAuthSvc.GetMe(ctx) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -157,30 +160,30 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string } return WailsRequestRouterResponse{Body: me, Error: ""} case "/api/alby/balance": - balance, err := app.svc.AlbyOAuthSvc.GetBalance(ctx) + balance, err := app.svc.albyOAuthSvc.GetBalance(ctx) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } - return WailsRequestRouterResponse{Body: &api.AlbyBalanceResponse{ + return WailsRequestRouterResponse{Body: &alby.AlbyBalanceResponse{ Sats: balance.Balance, }, Error: ""} case "/api/alby/pay": - payRequest := &api.AlbyPayRequest{} + payRequest := &alby.AlbyPayRequest{} err := json.Unmarshal([]byte(body), payRequest) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } - err = app.svc.AlbyOAuthSvc.SendPayment(ctx, payRequest.Invoice) + err = app.svc.albyOAuthSvc.SendPayment(ctx, payRequest.Invoice) if err != nil { return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -197,7 +200,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string createAppRequest := &api.CreateAppRequest{} err := json.Unmarshal([]byte(body), createAppRequest) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -214,7 +217,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string resetRouterRequest := &api.ResetRouterRequest{} err := json.Unmarshal([]byte(body), resetRouterRequest) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -222,7 +225,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } - err = app.api.ResetRouter(resetRouterRequest.Key, true) + err = app.api.ResetRouter(resetRouterRequest.Key) if err != nil { return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -248,7 +251,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string openChannelRequest := &api.OpenChannelRequest{} err := json.Unmarshal([]byte(body), openChannelRequest) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -289,7 +292,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string redeemOnchainFundsRequest := &api.RedeemOnchainFundsRequest{} err := json.Unmarshal([]byte(body), redeemOnchainFundsRequest) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -310,6 +313,15 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } return WailsRequestRouterResponse{Body: *signMessageResponse, Error: ""} + // TODO: review naming + case "/api/instant-channel-invoices": + newInstantChannelRequest := &lsp.NewInstantChannelInvoiceRequest{} + err := json.Unmarshal([]byte(body), newInstantChannelRequest) + newInstantChannelResponseResponse, err := app.api.GetLSPService().NewInstantChannelInvoice(ctx, newInstantChannelRequest) + if err != nil { + return WailsRequestRouterResponse{Body: nil, Error: err.Error()} + } + return WailsRequestRouterResponse{Body: *newInstantChannelResponseResponse, Error: ""} case "/api/peers": switch method { case "GET": @@ -322,7 +334,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string connectPeerRequest := &api.ConnectPeerRequest{} err := json.Unmarshal([]byte(body), connectPeerRequest) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -356,7 +368,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string res := WailsRequestRouterResponse{Body: *infoResponse, Error: ""} return res case "/api/alby/link-account": - err := app.svc.AlbyOAuthSvc.LinkAccount(ctx) + err := app.svc.albyOAuthSvc.LinkAccount(ctx) if err != nil { return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -370,7 +382,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string backupReminderRequest := &api.BackupReminderRequest{} err := json.Unmarshal([]byte(body), backupReminderRequest) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -380,7 +392,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string err = app.api.SetNextBackupReminder(backupReminderRequest) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -392,7 +404,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string changeUnlockPasswordRequest := &api.ChangeUnlockPasswordRequest{} err := json.Unmarshal([]byte(body), changeUnlockPasswordRequest) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -402,7 +414,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string err = app.api.ChangeUnlockPassword(changeUnlockPasswordRequest) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -414,7 +426,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string startRequest := &api.StartRequest{} err := json.Unmarshal([]byte(body), startRequest) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -423,7 +435,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string } err = app.api.Start(startRequest) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -437,7 +449,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string setupRequest := &api.SetupRequest{} err := json.Unmarshal([]byte(body), setupRequest) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -446,7 +458,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string } err = app.api.Setup(ctx, setupRequest) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -458,7 +470,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string sendPaymentProbesRequest := &api.SendPaymentProbesRequest{} err := json.Unmarshal([]byte(body), sendPaymentProbesRequest) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -467,7 +479,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string } sendPaymentProbesResponse, err := app.api.SendPaymentProbes(ctx, sendPaymentProbesRequest) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -479,7 +491,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string sendSpontaneousPaymentProbesRequest := &api.SendSpontaneousPaymentProbesRequest{} err := json.Unmarshal([]byte(body), sendSpontaneousPaymentProbesRequest) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -488,7 +500,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string } sendSpontaneousPaymentProbesResponse, err := app.api.SendSpontaneousPaymentProbes(ctx, sendSpontaneousPaymentProbesRequest) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -500,7 +512,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string backupRequest := &api.BasicBackupRequest{} err := json.Unmarshal([]byte(body), backupRequest) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -513,7 +525,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string DefaultFilename: "nwc.bkp", }) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -523,7 +535,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string backupFile, err := os.Create(saveFilePath) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -533,14 +545,10 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string defer backupFile.Close() - backupReq := api.BasicBackupRequest{ - UnlockPassword: backupRequest.UnlockPassword, - } - - err = app.api.CreateBackup(&backupReq, backupFile) + err = app.api.GetBackupService().CreateBackup(backupRequest.UnlockPassword, backupFile) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -552,7 +560,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string restoreRequest := &api.BasicRestoreWailsRequest{} err := json.Unmarshal([]byte(body), restoreRequest) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -565,7 +573,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string DefaultFilename: "nwc.bkp", }) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -575,7 +583,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string backupFile, err := os.Open(backupFilePath) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -585,9 +593,9 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string defer backupFile.Close() - err = app.api.RestoreBackup(restoreRequest.UnlockPassword, backupFile) + err = app.api.GetBackupService().RestoreBackup(restoreRequest.UnlockPassword, backupFile) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -605,7 +613,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string getLogOutputRequest := &api.GetLogOutputRequest{} err := json.Unmarshal([]byte(body), getLogOutputRequest) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -614,7 +622,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string } logOutputResponse, err := app.api.GetLogOutput(ctx, logType, getLogOutputRequest) if err != nil { - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -624,7 +632,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string return WailsRequestRouterResponse{Body: logOutputResponse, Error: ""} } - app.svc.Logger.WithFields(logrus.Fields{ + app.svc.logger.WithFields(logrus.Fields{ "route": route, "method": method, }).Error("Unhandled route")