diff --git a/.github/workflows/build-docker.yaml b/.github/workflows/build-docker.yaml index 40484debe..bce244ff5 100644 --- a/.github/workflows/build-docker.yaml +++ b/.github/workflows/build-docker.yaml @@ -19,7 +19,7 @@ jobs: - name: Checkout code uses: actions/checkout@v2 - name: Run tests - run: mkdir frontend/dist && touch frontend/dist/tmp && go test + run: mkdir frontend/dist && touch frontend/dist/tmp && go test ./... - name: Docker build uses: mr-smithers-excellent/docker-build-push@v6 id: build diff --git a/.github/workflows/multiplatform.yaml b/.github/workflows/multiplatform.yaml index 63ffd9985..e8f94cd1b 100644 --- a/.github/workflows/multiplatform.yaml +++ b/.github/workflows/multiplatform.yaml @@ -19,7 +19,7 @@ jobs: - name: Checkout code uses: actions/checkout@v2 - name: Run tests - run: mkdir frontend/dist && touch frontend/dist/tmp && go test + run: mkdir frontend/dist && touch frontend/dist/tmp && go test ./... - name: Docker build uses: mr-smithers-excellent/docker-build-push@v6 id: build diff --git a/.github/workflows/package-raspberry-pi.yml b/.github/workflows/package-raspberry-pi.yml index dd10d0c03..0561bfe42 100644 --- a/.github/workflows/package-raspberry-pi.yml +++ b/.github/workflows/package-raspberry-pi.yml @@ -42,7 +42,7 @@ jobs: GOARM: 6 CGO_ENABLED: 1 CC: ${{ github.workspace }}/armv6-unknown-linux-gnueabihf/bin/armv6-unknown-linux-gnueabihf-gcc - run: go build -tags skip_breez,netgo -v -o nostr-wallet-connect + run: go build -tags skip_breez,netgo -v -o nostr-wallet-connect cmd/http/main.go - name: Find and copy shared libraries run: | diff --git a/.vscode/settings.json b/.vscode/settings.json index 7624bc9fa..d979d7974 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,7 +4,6 @@ "editor.defaultFormatter": "golang.go" }, "editor.formatOnSave": true, - "go.buildTags": "http,wails", "typescript.preferences.importModuleSpecifier": "non-relative", "editor.codeActionsOnSave": { "source.organizeImports": "always" diff --git a/Dockerfile b/Dockerfile index 6796e989f..8f5b82f29 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,7 +33,7 @@ COPY . . # Copy frontend dist files into the container COPY --from=frontend /build/frontend/dist ./frontend/dist -RUN GOARCH=$(echo "$TARGETPLATFORM" | cut -d'/' -f2) go build -o main . +RUN GOARCH=$(echo "$TARGETPLATFORM" | cut -d'/' -f2) go build -o main cmd/http/main.go RUN cp `find /go/pkg/mod/github.com/breez/ |grep linux-amd64 |grep libbreez_sdk_bindings.so` ./ RUN cp `find /go/pkg/mod/github.com/get\!alby/ | grep x86_64-unknown-linux-gnu | grep libglalby_bindings.so` ./ diff --git a/README.md b/README.md index 4a55c3fb6..595a08713 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,6 @@ As data storage SQLite is used. # edit the config for your needs vim .env -#### Optional Requirements - -See [Greenlight](./README_GREENLIGHT.md) - ## Development ### Required Software @@ -53,7 +49,7 @@ See [Greenlight](./README_GREENLIGHT.md) 2. Compile the frontend or run `touch frontend/dist/tmp` to ensure there are embeddable files available. -3. `go run .` +3. `go run cmd/http/main.go` ### React Frontend (HTTP mode) @@ -77,7 +73,7 @@ _If you get a blank screen, try running in your normal terminal (outside of vsco ### Build and run locally (HTTP mode) $ mkdir tmp - $ go build -o main + $ go build -o main cmd/http/main.go $ cp main tmp $ cp .env tmp $ cd tmp @@ -90,7 +86,11 @@ _If you get a blank screen, try running in your normal terminal (outside of vsco ### Testing - $ go test + $ go test ./... + +#### Test matching regular expression + + $ go test ./... -run TestHandleGetInfoEvent ### Profiling @@ -363,8 +363,9 @@ LDK logs: - install nvm (curl script) - with nvm, choose node lts - install yarn (via npm) -- then run yarn build -- go run . +- run `(cd frontend && yarn install` +- run `(cd frontend && yarn build:http)` +- run `go run cmd/http/main.go` ### Docker diff --git a/README_GREENLIGHT.md b/README_GREENLIGHT.md deleted file mode 100644 index bbc9292e8..000000000 --- a/README_GREENLIGHT.md +++ /dev/null @@ -1,17 +0,0 @@ -# Greenlight - -To enable the GREENLIGHT LNClient some additional steps are required. - -## Required Software - -- Python 3 + pip -- [Greenlight Client](https://github.com/Blockstream/greenlight/tree/main?tab=readme-ov-file#install-and-updating-glcli-and-python-api) with [fix commit](https://github.com/Blockstream/greenlight/commit/2dc5a94668d41baef7275dae860c09b4a5dba198) - -## Setup - -1. Wallet can be registered or recovered through NWC UI -2. Get Liquidity - 1. peer with blocktank (TBC): `glcli connect 0296b2db342fcf87ea94d981757fdf4d3e545bd5cef4919f58b5d38dfdd73bf5c9 130.211.95.29 9735` - 2. run `glcli scheduler schedule` to get your node ID and GRPC uri - 3. go to blocktank and pay for a channel. After paying invoice, claim manually. Format is without the https: `pubkey@domain_name:9735` - 4. Unless you paid for some outgoing liquidity, you'll need to deposit and reserve at least 1% of the channel balance to send outgoing payments. diff --git a/alby/alby_http_service.go b/alby/alby_http_service.go index 08597784b..01649aee1 100644 --- a/alby/alby_http_service.go +++ b/alby/alby_http_service.go @@ -5,20 +5,18 @@ import ( "net/http" "github.com/getAlby/nostr-wallet-connect/config" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/labstack/echo/v4" - "github.com/sirupsen/logrus" ) type AlbyHttpService struct { albyOAuthSvc AlbyOAuthService - logger *logrus.Logger appConfig *config.AppConfig } -func NewAlbyHttpService(albyOAuthSvc AlbyOAuthService, logger *logrus.Logger, appConfig *config.AppConfig) *AlbyHttpService { +func NewAlbyHttpService(albyOAuthSvc AlbyOAuthService, appConfig *config.AppConfig) *AlbyHttpService { return &AlbyHttpService{ albyOAuthSvc: albyOAuthSvc, - logger: logger, appConfig: appConfig, } } @@ -36,7 +34,7 @@ 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") + logger.Logger.WithError(err).Error("Failed to handle Alby OAuth callback") return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to handle Alby OAuth callback: %s", err.Error()), }) @@ -59,7 +57,7 @@ func (albyHttpSvc *AlbyHttpService) albyCallbackHandler(c echo.Context) error { 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") + logger.Logger.WithError(err).Error("Failed to request alby me endpoint") return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to request alby me endpoint: %s", err.Error()), }) @@ -71,7 +69,7 @@ func (albyHttpSvc *AlbyHttpService) albyMeHandler(c echo.Context) error { 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") + logger.Logger.WithError(err).Error("Failed to request alby balance endpoint") return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to request alby balance endpoint: %s", err.Error()), }) @@ -92,7 +90,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") + logger.Logger.WithError(err).Error("Failed to request alby pay endpoint") return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to request alby pay endpoint: %s", err.Error()), }) @@ -104,7 +102,7 @@ func (albyHttpSvc *AlbyHttpService) albyPayHandler(c echo.Context) error { func (albyHttpSvc *AlbyHttpService) albyLinkAccountHandler(c echo.Context) error { err := albyHttpSvc.albyOAuthSvc.LinkAccount(c.Request().Context()) if err != nil { - albyHttpSvc.logger.WithError(err).Error("Failed to connect alby account") + logger.Logger.WithError(err).Error("Failed to connect alby account") return err } diff --git a/alby/alby_oauth_service.go b/alby/alby_oauth_service.go index 75fa1679c..27ee75adc 100644 --- a/alby/alby_oauth_service.go +++ b/alby/alby_oauth_service.go @@ -14,19 +14,21 @@ import ( "github.com/sirupsen/logrus" "golang.org/x/oauth2" + "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/nip47" + "github.com/getAlby/nostr-wallet-connect/logger" + nip47 "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/getAlby/nostr-wallet-connect/service/keys" ) type albyOAuthService struct { - appConfig *config.AppConfig - config config.Config + cfg config.Config oauthConf *oauth2.Config - logger *logrus.Logger - dbSvc db.DBService + db *gorm.DB + keys keys.Keys } const ( @@ -36,30 +38,29 @@ const ( userIdentifierKey = "AlbyUserIdentifier" ) -func NewAlbyOAuthService(logger *logrus.Logger, config config.Config, appConfig *config.AppConfig, dbSvc db.DBService) *albyOAuthService { +func NewAlbyOAuthService(db *gorm.DB, cfg config.Config, keys keys.Keys) *albyOAuthService { conf := &oauth2.Config{ - ClientID: appConfig.AlbyClientId, - ClientSecret: appConfig.AlbyClientSecret, + ClientID: cfg.GetEnv().AlbyClientId, + ClientSecret: cfg.GetEnv().AlbyClientSecret, Scopes: []string{"account:read", "balance:read", "payments:send"}, Endpoint: oauth2.Endpoint{ - TokenURL: appConfig.AlbyAPIURL + "/oauth/token", - AuthURL: appConfig.AlbyOAuthAuthUrl, + TokenURL: cfg.GetEnv().AlbyAPIURL + "/oauth/token", + AuthURL: cfg.GetEnv().AlbyOAuthAuthUrl, AuthStyle: 2, // use HTTP Basic Authorization https://pkg.go.dev/golang.org/x/oauth2#AuthStyle }, } - if appConfig.IsDefaultClientId() { + if cfg.GetEnv().IsDefaultClientId() { conf.RedirectURL = "https://getalby.com/hub/callback" } else { - conf.RedirectURL = appConfig.BaseUrl + "/api/alby/callback" + conf.RedirectURL = cfg.GetEnv().BaseUrl + "/api/alby/callback" } albyOAuthSvc := &albyOAuthService{ - appConfig: appConfig, oauthConf: conf, - config: config, - logger: logger, - dbSvc: dbSvc, + cfg: cfg, + db: db, + keys: keys, } return albyOAuthSvc } @@ -67,31 +68,31 @@ func NewAlbyOAuthService(logger *logrus.Logger, config config.Config, appConfig 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") + logger.Logger.WithError(err).Error("Failed to exchange token") return err } svc.saveToken(token) me, err := svc.GetMe(ctx) if err != nil { - svc.logger.WithError(err).Error("Failed to fetch user me") + logger.Logger.WithError(err).Error("Failed to fetch user me") // remove token so user can retry - svc.config.SetUpdate(accessTokenKey, "", "") + svc.cfg.SetUpdate(accessTokenKey, "", "") return err } existingUserIdentifier, err := svc.GetUserIdentifier() if err != nil { - svc.logger.WithError(err).Error("Failed to get alby user identifier") + logger.Logger.WithError(err).Error("Failed to get alby user identifier") return err } // save the user's alby account ID on first time login if existingUserIdentifier == "" { - svc.config.SetUpdate(userIdentifierKey, me.Identifier, "") + svc.cfg.SetUpdate(userIdentifierKey, me.Identifier, "") } else if me.Identifier != existingUserIdentifier { // remove token so user can retry with correct account - svc.config.SetUpdate(accessTokenKey, "", "") + svc.cfg.SetUpdate(accessTokenKey, "", "") return errors.New("Alby Hub is connected to a different alby account. Please log out of your Alby Account at getalby.com and try again.") } @@ -99,9 +100,9 @@ func (svc *albyOAuthService) CallbackHandler(ctx context.Context, code string) e } func (svc *albyOAuthService) GetUserIdentifier() (string, error) { - userIdentifier, err := svc.config.Get(userIdentifierKey, "") + userIdentifier, err := svc.cfg.Get(userIdentifierKey, "") if err != nil { - svc.logger.WithError(err).Error("Failed to fetch user identifier from user configs") + logger.Logger.WithError(err).Error("Failed to fetch user identifier from user configs") return "", err } return userIdentifier, nil @@ -110,15 +111,15 @@ func (svc *albyOAuthService) GetUserIdentifier() (string, error) { 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") + logger.Logger.WithError(err).Error("Failed to check fetch token") } return token != nil } 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, "") + svc.cfg.SetUpdate(accessTokenExpiryKey, strconv.FormatInt(token.Expiry.Unix(), 10), "") + svc.cfg.SetUpdate(accessTokenKey, token.AccessToken, "") + svc.cfg.SetUpdate(refreshTokenKey, token.RefreshToken, "") } var tokenMutex sync.Mutex @@ -126,7 +127,7 @@ var tokenMutex sync.Mutex func (svc *albyOAuthService) fetchUserToken(ctx context.Context) (*oauth2.Token, error) { tokenMutex.Lock() defer tokenMutex.Unlock() - accessToken, err := svc.config.Get(accessTokenKey, "") + accessToken, err := svc.cfg.Get(accessTokenKey, "") if err != nil { return nil, err } @@ -135,7 +136,7 @@ func (svc *albyOAuthService) fetchUserToken(ctx context.Context) (*oauth2.Token, return nil, nil } - expiry, err := svc.config.Get(accessTokenExpiryKey, "") + expiry, err := svc.cfg.Get(accessTokenExpiryKey, "") if err != nil { return nil, err } @@ -148,7 +149,7 @@ func (svc *albyOAuthService) fetchUserToken(ctx context.Context) (*oauth2.Token, if err != nil { return nil, err } - refreshToken, err := svc.config.Get(refreshTokenKey, "") + refreshToken, err := svc.cfg.Get(refreshTokenKey, "") if err != nil { return nil, err } @@ -164,13 +165,13 @@ func (svc *albyOAuthService) fetchUserToken(ctx context.Context) (*oauth2.Token, } if currentToken.Expiry.After(time.Now().Add(time.Duration(1) * time.Second)) { - svc.logger.Info("Using existing Alby OAuth token") + logger.Logger.Info("Using existing Alby OAuth token") return currentToken, nil } newToken, err := svc.oauthConf.TokenSource(ctx, currentToken).Token() if err != nil { - svc.logger.WithError(err).Error("Failed to refresh existing token") + logger.Logger.WithError(err).Error("Failed to refresh existing token") return nil, err } @@ -181,15 +182,15 @@ func (svc *albyOAuthService) fetchUserToken(ctx context.Context) (*oauth2.Token, 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") + logger.Logger.WithError(err).Error("Failed to fetch user token") return nil, err } client := svc.oauthConf.Client(ctx, token) - req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/users", svc.appConfig.AlbyAPIURL), nil) + req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/users", svc.cfg.GetEnv().AlbyAPIURL), nil) if err != nil { - svc.logger.WithError(err).Error("Error creating request /me") + logger.Logger.WithError(err).Error("Error creating request /me") return nil, err } @@ -197,18 +198,18 @@ func (svc *albyOAuthService) GetMe(ctx context.Context) (*AlbyMe, error) { res, err := client.Do(req) if err != nil { - svc.logger.WithError(err).Error("Failed to fetch /me") + logger.Logger.WithError(err).Error("Failed to fetch /me") return nil, err } me := &AlbyMe{} err = json.NewDecoder(res.Body).Decode(me) if err != nil { - svc.logger.WithError(err).Error("Failed to decode API response") + logger.Logger.WithError(err).Error("Failed to decode API response") return nil, err } - svc.logger.WithFields(logrus.Fields{"me": me}).Info("Alby me response") + logger.Logger.WithFields(logrus.Fields{"me": me}).Info("Alby me response") return me, nil } @@ -216,15 +217,15 @@ func (svc *albyOAuthService) GetBalance(ctx context.Context) (*AlbyBalance, erro token, err := svc.fetchUserToken(ctx) if err != nil { - svc.logger.WithError(err).Error("Failed to fetch user token") + logger.Logger.WithError(err).Error("Failed to fetch user token") return nil, err } client := svc.oauthConf.Client(ctx, token) - req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/lndhub/balance", svc.appConfig.AlbyAPIURL), nil) + req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/lndhub/balance", svc.cfg.GetEnv().AlbyAPIURL), nil) if err != nil { - svc.logger.WithError(err).Error("Error creating request to balance endpoint") + logger.Logger.WithError(err).Error("Error creating request to balance endpoint") return nil, err } @@ -232,24 +233,24 @@ func (svc *albyOAuthService) GetBalance(ctx context.Context) (*AlbyBalance, erro res, err := client.Do(req) if err != nil { - svc.logger.WithError(err).Error("Failed to fetch balance endpoint") + logger.Logger.WithError(err).Error("Failed to fetch balance endpoint") return nil, err } balance := &AlbyBalance{} err = json.NewDecoder(res.Body).Decode(balance) if err != nil { - svc.logger.WithError(err).Error("Failed to decode API response") + logger.Logger.WithError(err).Error("Failed to decode API response") return nil, err } - svc.logger.WithFields(logrus.Fields{"balance": balance}).Info("Alby balance response") + logger.Logger.WithFields(logrus.Fields{"balance": balance}).Info("Alby balance response") return balance, nil } 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") + logger.Logger.WithError(err).Error("Failed to fetch user token") return err } @@ -266,13 +267,13 @@ func (svc *albyOAuthService) SendPayment(ctx context.Context, invoice string) er err = json.NewEncoder(body).Encode(&payload) if err != nil { - svc.logger.WithError(err).Error("Failed to encode request payload") + logger.Logger.WithError(err).Error("Failed to encode request payload") return err } - req, err := http.NewRequest("POST", fmt.Sprintf("%s/internal/lndhub/bolt11", svc.appConfig.AlbyAPIURL), body) + req, err := http.NewRequest("POST", fmt.Sprintf("%s/internal/lndhub/bolt11", svc.cfg.GetEnv().AlbyAPIURL), body) if err != nil { - svc.logger.WithError(err).Error("Error creating request bolt11 endpoint") + logger.Logger.WithError(err).Error("Error creating request bolt11 endpoint") return err } @@ -281,7 +282,7 @@ func (svc *albyOAuthService) SendPayment(ctx context.Context, invoice string) er resp, err := client.Do(req) if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "invoice": invoice, }).WithError(err).Error("Failed to pay invoice") return err @@ -303,13 +304,13 @@ func (svc *albyOAuthService) SendPayment(ctx context.Context, invoice string) er errorPayload := &ErrorResponse{} err = json.NewDecoder(resp.Body).Decode(errorPayload) if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "status": resp.StatusCode, }).WithError(err).Error("Failed to decode payment error response payload") return err } - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "invoice": invoice, "status": resp.StatusCode, "message": errorPayload.Message, @@ -320,10 +321,10 @@ func (svc *albyOAuthService) SendPayment(ctx context.Context, invoice string) er responsePayload := &PayResponse{} err = json.NewDecoder(resp.Body).Decode(responsePayload) if err != nil { - svc.logger.WithError(err).Error("Failed to decode response payload") + logger.Logger.WithError(err).Error("Failed to decode response payload") return err } - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "invoice": invoice, "paymentHash": responsePayload.PaymentHash, "preimage": responsePayload.Preimage, @@ -332,8 +333,8 @@ func (svc *albyOAuthService) SendPayment(ctx context.Context, invoice string) er } 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") + if svc.cfg.GetEnv().AlbyClientId == "" || svc.cfg.GetEnv().AlbyClientSecret == "" { + logger.Logger.Fatalf("No ALBY_OAUTH_CLIENT_ID or ALBY_OAUTH_CLIENT_SECRET set") } return svc.oauthConf.AuthCodeURL("unused") } @@ -341,11 +342,11 @@ func (svc *albyOAuthService) GetAuthUrl() string { 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") + logger.Logger.WithError(err).Error("Failed to create alby account nwc node") return err } - app, _, err := svc.dbSvc.CreateApp( + app, _, err := db.NewDBService(svc.db).CreateApp( "getalby.com", connectionPubkey, 1_000_000, @@ -355,17 +356,17 @@ func (svc *albyOAuthService) LinkAccount(ctx context.Context) error { ) if err != nil { - svc.logger.WithError(err).Error("Failed to create app connection") + logger.Logger.WithError(err).Error("Failed to create app connection") return err } - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "app": app, }).Info("Created alby app connection") err = svc.activateAlbyAccountNWCNode(ctx) if err != nil { - svc.logger.WithError(err).Error("Failed to activate alby account nwc node") + logger.Logger.WithError(err).Error("Failed to activate alby account nwc node") return err } @@ -374,14 +375,14 @@ func (svc *albyOAuthService) LinkAccount(ctx context.Context) 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") + if !svc.cfg.GetEnv().LogEvents { + logger.Logger.WithField("event", event).Debug("Skipped sending to alby events API") return nil } if event.Event == "nwc_backup_channels" { if err := svc.backupChannels(ctx, event); err != nil { - svc.logger.WithError(err).Error("Failed to backup channels") + logger.Logger.WithError(err).Error("Failed to backup channels") return err } return nil @@ -389,7 +390,7 @@ func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Eve token, err := svc.fetchUserToken(ctx) if err != nil { - svc.logger.WithError(err).Error("Failed to fetch user token") + logger.Logger.WithError(err).Error("Failed to fetch user token") return err } @@ -400,7 +401,7 @@ func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Eve err = json.NewEncoder(originalEventBuffer).Encode(event) if err != nil { - svc.logger.WithError(err).Error("Failed to encode request payload") + logger.Logger.WithError(err).Error("Failed to encode request payload") return err } @@ -412,7 +413,7 @@ func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Eve var eventWithGlobalProperties eventWithPropertiesMap err = json.Unmarshal(originalEventBuffer.Bytes(), &eventWithGlobalProperties) if err != nil { - svc.logger.WithError(err).Error("Failed to decode request payload") + logger.Logger.WithError(err).Error("Failed to decode request payload") return err } if eventWithGlobalProperties.Properties == nil { @@ -423,7 +424,7 @@ func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Eve for k, v := range globalProperties { _, exists := eventWithGlobalProperties.Properties[k] if exists { - svc.logger.WithField("key", k).Error("Key already exists in event properties, skipping global property") + logger.Logger.WithField("key", k).Error("Key already exists in event properties, skipping global property") continue } eventWithGlobalProperties.Properties[k] = v @@ -433,13 +434,13 @@ func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Eve err = json.NewEncoder(body).Encode(&eventWithGlobalProperties) if err != nil { - svc.logger.WithError(err).Error("Failed to encode request payload") + logger.Logger.WithError(err).Error("Failed to encode request payload") return err } - req, err := http.NewRequest("POST", fmt.Sprintf("%s/events", svc.appConfig.AlbyAPIURL), body) + req, err := http.NewRequest("POST", fmt.Sprintf("%s/events", svc.cfg.GetEnv().AlbyAPIURL), body) if err != nil { - svc.logger.WithError(err).Error("Error creating request /events") + logger.Logger.WithError(err).Error("Error creating request /events") return err } @@ -448,14 +449,14 @@ func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Eve resp, err := client.Do(req) if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "event": eventWithGlobalProperties, }).WithError(err).Error("Failed to send request to /events") return err } if resp.StatusCode >= 300 { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "event": eventWithGlobalProperties, "status": resp.StatusCode, }).Error("Request to /events returned non-success status") @@ -490,7 +491,7 @@ func (svc *albyOAuthService) backupChannels(ctx context.Context, event *events.E } // use the encrypted mnemonic as the password to encrypt the backup data - encryptedMnemonic, err := svc.config.Get("Mnemonic", "") + encryptedMnemonic, err := svc.cfg.Get("Mnemonic", "") if err != nil { return fmt.Errorf("failed to fetch encryption key: %w", err) } @@ -509,7 +510,7 @@ func (svc *albyOAuthService) backupChannels(ctx context.Context, event *events.E return fmt.Errorf("failed to encode channels backup request payload: %w", err) } - req, err := http.NewRequest("POST", fmt.Sprintf("%s/internal/backups", svc.appConfig.AlbyAPIURL), body) + req, err := http.NewRequest("POST", fmt.Sprintf("%s/internal/backups", svc.cfg.GetEnv().AlbyAPIURL), body) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -532,7 +533,7 @@ func (svc *albyOAuthService) backupChannels(ctx context.Context, event *events.E 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") + logger.Logger.WithError(err).Error("Failed to fetch user token") } client := svc.oauthConf.Client(ctx, token) @@ -542,20 +543,20 @@ func (svc *albyOAuthService) createAlbyAccountNWCNode(ctx context.Context) (stri } createNodeRequest := createNWCNodeRequest{ - WalletPubkey: svc.config.GetNostrPublicKey(), + WalletPubkey: svc.keys.GetNostrPublicKey(), } body := bytes.NewBuffer([]byte{}) err = json.NewEncoder(body).Encode(&createNodeRequest) if err != nil { - svc.logger.WithError(err).Error("Failed to encode request payload") + logger.Logger.WithError(err).Error("Failed to encode request payload") return "", err } - req, err := http.NewRequest("POST", fmt.Sprintf("%s/internal/nwcs", svc.appConfig.AlbyAPIURL), body) + req, err := http.NewRequest("POST", fmt.Sprintf("%s/internal/nwcs", svc.cfg.GetEnv().AlbyAPIURL), body) if err != nil { - svc.logger.WithError(err).Error("Error creating request /internal/nwcs") + logger.Logger.WithError(err).Error("Error creating request /internal/nwcs") return "", err } @@ -564,14 +565,14 @@ func (svc *albyOAuthService) createAlbyAccountNWCNode(ctx context.Context) (stri resp, err := client.Do(req) if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "createNodeRequest": createNodeRequest, }).WithError(err).Error("Failed to send request to /internal/nwcs") return "", err } if resp.StatusCode >= 300 { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "createNodeRequest": createNodeRequest, "status": resp.StatusCode, }).Error("Request to /internal/nwcs returned non-success status") @@ -585,11 +586,11 @@ func (svc *albyOAuthService) createAlbyAccountNWCNode(ctx context.Context) (stri responsePayload := &CreateNWCNodeResponse{} err = json.NewDecoder(resp.Body).Decode(responsePayload) if err != nil { - svc.logger.WithError(err).Error("Failed to decode response payload") + logger.Logger.WithError(err).Error("Failed to decode response payload") return "", err } - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "pubkey": responsePayload.Pubkey, }).Info("Created alby nwc node successfully") @@ -599,14 +600,14 @@ func (svc *albyOAuthService) createAlbyAccountNWCNode(ctx context.Context) (stri 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") + logger.Logger.WithError(err).Error("Failed to fetch user token") } client := svc.oauthConf.Client(ctx, token) - req, err := http.NewRequest("PUT", fmt.Sprintf("%s/internal/nwcs/activate", svc.appConfig.AlbyAPIURL), nil) + req, err := http.NewRequest("PUT", fmt.Sprintf("%s/internal/nwcs/activate", svc.cfg.GetEnv().AlbyAPIURL), nil) if err != nil { - svc.logger.WithError(err).Error("Error creating request /internal/nwcs/activate") + logger.Logger.WithError(err).Error("Error creating request /internal/nwcs/activate") return err } @@ -615,18 +616,18 @@ func (svc *albyOAuthService) activateAlbyAccountNWCNode(ctx context.Context) err resp, err := client.Do(req) if err != nil { - svc.logger.WithError(err).Error("Failed to send request to /internal/nwcs/activate") + logger.Logger.WithError(err).Error("Failed to send request to /internal/nwcs/activate") return err } if resp.StatusCode >= 300 { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "status": resp.StatusCode, }).Error("Request to /internal/nwcs/activate returned non-success status") return errors.New("request to /internal/nwcs/activate returned non-success status") } - svc.logger.Info("Activated alby nwc node successfully") + logger.Logger.Info("Activated alby nwc node successfully") return nil } @@ -635,15 +636,15 @@ func (svc *albyOAuthService) GetChannelPeerSuggestions(ctx context.Context) ([]C token, err := svc.fetchUserToken(ctx) if err != nil { - svc.logger.WithError(err).Error("Failed to fetch user token") + logger.Logger.WithError(err).Error("Failed to fetch user token") return nil, err } client := svc.oauthConf.Client(ctx, token) - req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/channel_suggestions", svc.appConfig.AlbyAPIURL), nil) + req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/channel_suggestions", svc.cfg.GetEnv().AlbyAPIURL), nil) if err != nil { - svc.logger.WithError(err).Error("Error creating request to channel_suggestions endpoint") + logger.Logger.WithError(err).Error("Error creating request to channel_suggestions endpoint") return nil, err } @@ -651,16 +652,16 @@ func (svc *albyOAuthService) GetChannelPeerSuggestions(ctx context.Context) ([]C res, err := client.Do(req) if err != nil { - svc.logger.WithError(err).Error("Failed to fetch channel_suggestions endpoint") + logger.Logger.WithError(err).Error("Failed to fetch channel_suggestions endpoint") return nil, err } var suggestions []ChannelPeerSuggestion err = json.NewDecoder(res.Body).Decode(&suggestions) if err != nil { - svc.logger.WithError(err).Errorf("Failed to decode API response") + logger.Logger.WithError(err).Errorf("Failed to decode API response") return nil, err } - svc.logger.WithFields(logrus.Fields{"channel_suggestions": suggestions}).Info("Alby channel peer suggestions response") + logger.Logger.WithFields(logrus.Fields{"channel_suggestions": suggestions}).Info("Alby channel peer suggestions response") return suggestions, nil } diff --git a/api/api.go b/api/api.go index 2cc11fa61..b420698cc 100644 --- a/api/api.go +++ b/api/api.go @@ -15,34 +15,37 @@ import ( "gorm.io/gorm" "github.com/getAlby/nostr-wallet-connect/alby" - "github.com/getAlby/nostr-wallet-connect/backup" "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/lsp" - "github.com/getAlby/nostr-wallet-connect/nip47" + "github.com/getAlby/nostr-wallet-connect/logger" + nip47 "github.com/getAlby/nostr-wallet-connect/nip47/models" + permissions "github.com/getAlby/nostr-wallet-connect/nip47/permissions" "github.com/getAlby/nostr-wallet-connect/service" + "github.com/getAlby/nostr-wallet-connect/service/keys" "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 + db *gorm.DB + dbSvc db.DBService + cfg config.Config + svc service.Service + permissionsSvc permissions.PermissionsService + keys keys.Keys + albyOAuthSvc alby.AlbyOAuthService } -func NewAPI(svc service.Service, logger *logrus.Logger, gormDb *gorm.DB) *api { - +func NewAPI(svc service.Service, gormDB *gorm.DB, config config.Config, keys keys.Keys, albyOAuthSvc alby.AlbyOAuthService, eventsPublisher events.EventPublisher) *api { return &api{ - svc: svc, - logger: logger, - db: gormDb, - dbSvc: db.NewDBService(gormDb, logger), - lspSvc: lsp.NewLSPService(svc, logger), - backupSvc: backup.NewBackupService(svc, logger), + db: gormDB, + dbSvc: db.NewDBService(gormDB), + cfg: config, + svc: svc, + permissionsSvc: permissions.NewPermissionsService(gormDB, eventsPublisher), + keys: keys, + albyOAuthSvc: albyOAuthSvc, } } @@ -58,13 +61,21 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons return nil, fmt.Errorf("won't create an app without request methods") } + for _, m := range requestMethods { + // TODO: this should be backend-specific + //if we don't know this method, we return an error + if !strings.Contains(nip47.CAPABILITIES, m) { + return nil, fmt.Errorf("did not recognize request method: %s", m) + } + } + 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() + relayUrl := api.cfg.GetRelayUrl() responseBody := &CreateAppResponse{} responseBody.Name = createAppRequest.Name @@ -76,7 +87,7 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons if err == nil { query := returnToUrl.Query() query.Add("relay", relayUrl) - query.Add("pubkey", api.svc.GetConfig().GetNostrPublicKey()) + query.Add("pubkey", api.keys.GetNostrPublicKey()) // if user.LightningAddress != "" { // query.Add("lud16", user.LightningAddress) // } @@ -89,7 +100,7 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons // 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) + responseBody.PairingUri = fmt.Sprintf("nostr+walletconnect://%s?relay=%s&secret=%s%s", api.keys.GetNostrPublicKey(), relayUrl, pairingSecretKey, lud16) return responseBody, nil } @@ -136,7 +147,7 @@ func (api *api) UpdateApp(userApp *db.App, updateAppRequest *UpdateAppRequest) e App: *userApp, RequestMethod: method, ExpiresAt: expiresAt, - MaxAmount: maxAmount, + MaxAmount: int(maxAmount), BudgetRenewal: budgetRenewal, } if err := tx.Create(&perm).Error; err != nil { @@ -185,10 +196,10 @@ func (api *api) GetApp(userApp *db.App) *App { } //renewsIn := "" - budgetUsage := int64(0) - maxAmount := paySpecificPermission.MaxAmount + budgetUsage := uint64(0) + maxAmount := uint64(paySpecificPermission.MaxAmount) if maxAmount > 0 { - budgetUsage = api.svc.GetBudgetUsage(&paySpecificPermission) + budgetUsage = api.permissionsSvc.GetBudgetUsage(&paySpecificPermission) } response := App{ @@ -241,9 +252,9 @@ func (api *api) ListApps() ([]App, error) { apiApp.ExpiresAt = permission.ExpiresAt if permission.RequestMethod == nip47.PAY_INVOICE_METHOD { apiApp.BudgetRenewal = permission.BudgetRenewal - apiApp.MaxAmount = permission.MaxAmount + apiApp.MaxAmount = uint64(permission.MaxAmount) if apiApp.MaxAmount > 0 { - apiApp.BudgetUsage = api.svc.GetBudgetUsage(&permission) + apiApp.BudgetUsage = api.permissionsSvc.GetBudgetUsage(&permission) } } } @@ -267,7 +278,7 @@ func (api *api) ListChannels(ctx context.Context) ([]lnclient.Channel, error) { } func (api *api) GetChannelPeerSuggestions(ctx context.Context) ([]alby.ChannelPeerSuggestion, error) { - return api.svc.GetAlbyOAuthSvc().GetChannelPeerSuggestions(ctx) + return api.albyOAuthSvc.GetChannelPeerSuggestions(ctx) } func (api *api) ResetRouter(key string) error { @@ -289,10 +300,10 @@ func (api *api) ChangeUnlockPassword(changeUnlockPasswordRequest *ChangeUnlockPa return errors.New("LNClient not started") } - err := api.svc.GetConfig().ChangeUnlockPassword(changeUnlockPasswordRequest.CurrentUnlockPassword, changeUnlockPasswordRequest.NewUnlockPassword) + err := api.cfg.ChangeUnlockPassword(changeUnlockPasswordRequest.CurrentUnlockPassword, changeUnlockPasswordRequest.NewUnlockPassword) if err != nil { - api.logger.WithError(err).Error("failed to change unlock password") + logger.Logger.WithError(err).Error("failed to change unlock password") return err } @@ -302,16 +313,19 @@ func (api *api) ChangeUnlockPassword(changeUnlockPasswordRequest *ChangeUnlockPa } func (api *api) Stop() error { - api.logger.Info("Running Stop command") + logger.Logger.Info("Running Stop command") if api.svc.GetLNClient() == nil { return errors.New("LNClient not started") } + + // TODO: this should stop everything related to the lnclient // 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") + logger.Logger.WithError(err).Error("Failed to stop LNClient") } + return err } @@ -354,7 +368,7 @@ func (api *api) DisconnectPeer(ctx context.Context, peerId string) error { if api.svc.GetLNClient() == nil { return errors.New("LNClient not started") } - api.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "peer_id": peerId, }).Info("Disconnecting peer") return api.svc.GetLNClient().DisconnectPeer(ctx, peerId) @@ -364,7 +378,7 @@ func (api *api) CloseChannel(ctx context.Context, peerId, channelId string, forc if api.svc.GetLNClient() == nil { return nil, errors.New("LNClient not started") } - api.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "peer_id": peerId, "channel_id": channelId, "force": force, @@ -385,7 +399,7 @@ func (api *api) GetNewOnchainAddress(ctx context.Context) (string, error) { return "", err } - api.svc.GetConfig().SetUpdate(config.OnchainAddressKey, address, "") + api.cfg.SetUpdate(config.OnchainAddressKey, address, "") return address, nil } @@ -395,9 +409,9 @@ func (api *api) GetUnusedOnchainAddress(ctx context.Context) (string, error) { return "", errors.New("LNClient not started") } - currentAddress, err := api.svc.GetConfig().Get(config.OnchainAddressKey, "") + currentAddress, err := api.cfg.Get(config.OnchainAddressKey, "") if err != nil { - api.logger.WithError(err).Error("Failed to get current address from config") + logger.Logger.WithError(err).Error("Failed to get current address from config") return "", err } @@ -405,13 +419,13 @@ func (api *api) GetUnusedOnchainAddress(ctx context.Context) (string, error) { // check if address has any transactions response, err := api.RequestEsploraApi("/address/" + currentAddress + "/txs") if err != nil { - api.logger.WithError(err).Error("Failed to get current address transactions") + logger.Logger.WithError(err).Error("Failed to get current address transactions") return currentAddress, nil } transactions, ok := response.([]interface{}) if !ok { - api.logger.WithField("response", response).Error("Failed to cast esplora address txs response", response) + logger.Logger.WithField("response", response).Error("Failed to cast esplora address txs response", response) return currentAddress, nil } @@ -423,7 +437,7 @@ func (api *api) GetUnusedOnchainAddress(ctx context.Context) (string, error) { newAddress, err := api.GetNewOnchainAddress(ctx) if err != nil { - api.logger.WithError(err).Error("Failed to retrieve new onchain address") + logger.Logger.WithError(err).Error("Failed to retrieve new onchain address") return "", err } return newAddress, nil @@ -469,7 +483,7 @@ func (api *api) GetBalances(ctx context.Context) (*BalancesResponse, error) { // TODO: remove dependency on this endpoint func (api *api) RequestMempoolApi(endpoint string) (interface{}, error) { - url := api.svc.GetConfig().GetEnv().MempoolApi + endpoint + url := api.cfg.GetEnv().MempoolApi + endpoint client := http.Client{ Timeout: time.Second * 10, @@ -477,7 +491,7 @@ func (api *api) RequestMempoolApi(endpoint string) (interface{}, error) { req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { - api.logger.WithError(err).WithFields(logrus.Fields{ + logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": url, }).Error("Failed to create http request") return nil, err @@ -485,7 +499,7 @@ func (api *api) RequestMempoolApi(endpoint string) (interface{}, error) { res, err := client.Do(req) if err != nil { - api.logger.WithError(err).WithFields(logrus.Fields{ + logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": url, }).Error("Failed to send request") return nil, err @@ -495,7 +509,7 @@ func (api *api) RequestMempoolApi(endpoint string) (interface{}, error) { body, readErr := io.ReadAll(res.Body) if readErr != nil { - api.logger.WithError(err).WithFields(logrus.Fields{ + logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": url, }).Error("Failed to read response body") return nil, errors.New("failed to read response body") @@ -504,7 +518,7 @@ func (api *api) RequestMempoolApi(endpoint string) (interface{}, error) { var jsonContent interface{} jsonErr := json.Unmarshal(body, &jsonContent) if jsonErr != nil { - api.logger.WithError(jsonErr).WithFields(logrus.Fields{ + logger.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)) @@ -514,44 +528,44 @@ func (api *api) RequestMempoolApi(endpoint string) (interface{}, error) { func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) { info := InfoResponse{} - backendType, _ := api.svc.GetConfig().Get("LNBackendType", "") - unlockPasswordCheck, _ := api.svc.GetConfig().Get("UnlockPasswordCheck", "") + backendType, _ := api.cfg.Get("LNBackendType", "") + unlockPasswordCheck, _ := api.cfg.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() + info.AlbyAuthUrl = api.albyOAuthSvc.GetAuthUrl() + info.OAuthRedirect = !api.cfg.GetEnv().IsDefaultClientId() + albyUserIdentifier, err := api.albyOAuthSvc.GetUserIdentifier() if err != nil { - api.logger.WithError(err).Error("Failed to get alby user identifier") + logger.Logger.WithError(err).Error("Failed to get alby user identifier") return nil, err } info.AlbyUserIdentifier = albyUserIdentifier - info.AlbyAccountConnected = api.svc.GetAlbyOAuthSvc().IsConnected(ctx) + info.AlbyAccountConnected = api.albyOAuthSvc.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") + logger.Logger.WithError(err).Error("Failed to get nodeInfo") return nil, err } info.Network = nodeInfo.Network } - info.NextBackupReminder, _ = api.svc.GetConfig().Get("NextBackupReminder", "") + info.NextBackupReminder, _ = api.cfg.Get("NextBackupReminder", "") return &info, nil } func (api *api) GetEncryptedMnemonic() *EncryptedMnemonicResponse { resp := EncryptedMnemonicResponse{} - mnemonic, _ := api.svc.GetConfig().Get("Mnemonic", "") + mnemonic, _ := api.cfg.Get("Mnemonic", "") resp.Mnemonic = mnemonic return &resp } func (api *api) SetNextBackupReminder(backupReminderRequest *BackupReminderRequest) error { - api.svc.GetConfig().SetUpdate("NextBackupReminder", backupReminderRequest.NextBackupReminder, "") + api.cfg.SetUpdate("NextBackupReminder", backupReminderRequest.NextBackupReminder, "") return nil } @@ -562,52 +576,52 @@ func (api *api) Start(startRequest *StartRequest) error { 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") + logger.Logger.WithError(err).Error("Failed to get info") return err } if info.SetupCompleted { - api.logger.Error("Cannot re-setup node") + logger.Logger.Error("Cannot re-setup node") return errors.New("setup already completed") } - api.svc.GetConfig().Setup(setupRequest.UnlockPassword) + api.cfg.Setup(setupRequest.UnlockPassword) // TODO: move all below code to cfg.Setup() // update next backup reminder - api.svc.GetConfig().SetUpdate("NextBackupReminder", setupRequest.NextBackupReminder, "") + api.cfg.SetUpdate("NextBackupReminder", setupRequest.NextBackupReminder, "") // only update non-empty values if setupRequest.LNBackendType != "" { - api.svc.GetConfig().SetUpdate("LNBackendType", setupRequest.LNBackendType, "") + api.cfg.SetUpdate("LNBackendType", setupRequest.LNBackendType, "") } if setupRequest.BreezAPIKey != "" { - api.svc.GetConfig().SetUpdate("BreezAPIKey", setupRequest.BreezAPIKey, setupRequest.UnlockPassword) + api.cfg.SetUpdate("BreezAPIKey", setupRequest.BreezAPIKey, setupRequest.UnlockPassword) } if setupRequest.Mnemonic != "" { - api.svc.GetConfig().SetUpdate("Mnemonic", setupRequest.Mnemonic, setupRequest.UnlockPassword) + api.cfg.SetUpdate("Mnemonic", setupRequest.Mnemonic, setupRequest.UnlockPassword) } if setupRequest.GreenlightInviteCode != "" { - api.svc.GetConfig().SetUpdate("GreenlightInviteCode", setupRequest.GreenlightInviteCode, setupRequest.UnlockPassword) + api.cfg.SetUpdate("GreenlightInviteCode", setupRequest.GreenlightInviteCode, setupRequest.UnlockPassword) } if setupRequest.LNDAddress != "" { - api.svc.GetConfig().SetUpdate("LNDAddress", setupRequest.LNDAddress, setupRequest.UnlockPassword) + api.cfg.SetUpdate("LNDAddress", setupRequest.LNDAddress, setupRequest.UnlockPassword) } if setupRequest.LNDCertHex != "" { - api.svc.GetConfig().SetUpdate("LNDCertHex", setupRequest.LNDCertHex, setupRequest.UnlockPassword) + api.cfg.SetUpdate("LNDCertHex", setupRequest.LNDCertHex, setupRequest.UnlockPassword) } if setupRequest.LNDMacaroonHex != "" { - api.svc.GetConfig().SetUpdate("LNDMacaroonHex", setupRequest.LNDMacaroonHex, setupRequest.UnlockPassword) + api.cfg.SetUpdate("LNDMacaroonHex", setupRequest.LNDMacaroonHex, setupRequest.UnlockPassword) } if setupRequest.PhoenixdAddress != "" { - api.svc.GetConfig().SetUpdate("PhoenixdAddress", setupRequest.PhoenixdAddress, setupRequest.UnlockPassword) + api.cfg.SetUpdate("PhoenixdAddress", setupRequest.PhoenixdAddress, setupRequest.UnlockPassword) } if setupRequest.PhoenixdAuthorization != "" { - api.svc.GetConfig().SetUpdate("PhoenixdAuthorization", setupRequest.PhoenixdAuthorization, setupRequest.UnlockPassword) + api.cfg.SetUpdate("PhoenixdAuthorization", setupRequest.PhoenixdAuthorization, setupRequest.UnlockPassword) } if setupRequest.CashuMintUrl != "" { - api.svc.GetConfig().SetUpdate("CashuMintUrl", setupRequest.CashuMintUrl, setupRequest.UnlockPassword) + api.cfg.SetUpdate("CashuMintUrl", setupRequest.CashuMintUrl, setupRequest.UnlockPassword) } return nil @@ -670,7 +684,7 @@ func (api *api) GetLogOutput(ctx context.Context, logType string, getLogRequest return nil, err } } else if logType == LogTypeApp { - logFileName := api.svc.GetLogFilePath() + logFileName := logger.GetLogFilePath() logData, err = utils.ReadFileTail(logFileName, getLogRequest.MaxLen) if err != nil { @@ -689,7 +703,7 @@ func (api *api) parseExpiresAt(expiresAtString string) (*time.Time, error) { var err error expiresAtValue, err := time.Parse(time.RFC3339, expiresAtString) if err != nil { - api.logger.WithField("expiresAt", expiresAtString).Error("Invalid expiresAt") + logger.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()) @@ -697,10 +711,3 @@ func (api *api) parseExpiresAt(expiresAtString string) (*time.Time, error) { } return expiresAt, nil } - -func (api *api) GetLSPService() lsp.LSPService { - return api.lspSvc -} -func (api *api) GetBackupService() backup.BackupService { - return api.backupSvc -} diff --git a/backup/backup_service.go b/api/backup.go similarity index 76% rename from backup/backup_service.go rename to api/backup.go index 9bb06ed47..b6b442802 100644 --- a/backup/backup_service.go +++ b/api/backup.go @@ -1,4 +1,4 @@ -package backup +package api import ( "errors" @@ -12,67 +12,54 @@ import ( "path/filepath" "slices" - "github.com/getAlby/nostr-wallet-connect/service" - "github.com/sirupsen/logrus" - "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" + "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/logger" "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 { +func (api *api) CreateBackup(unlockPassword string, w io.Writer) error { var err error - if !bs.svc.GetConfig().CheckUnlockPassword(unlockPassword) { + if !api.cfg.CheckUnlockPassword(unlockPassword) { return errors.New("invalid unlock password") } - workDir, err := filepath.Abs(bs.svc.GetConfig().GetEnv().Workdir) + workDir, err := filepath.Abs(api.cfg.GetEnv().Workdir) if err != nil { return fmt.Errorf("failed to get absolute workdir: %w", err) } lnStorageDir := "" - if bs.svc.GetLNClient() == nil { + if api.svc.GetLNClient() == nil { return fmt.Errorf("node not running") } - lnStorageDir, err = bs.svc.GetLNClient().GetStorageDir() + lnStorageDir, err = api.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") + logger.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") + err = api.svc.GetLNClient().ResetRouter("ALL") if err != nil { - bs.logger.WithError(err).Error("Failed to reset router") + logger.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() + api.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() + err = db.Stop(api.db) if err != nil { - bs.logger.WithError(err).Error("Failed to stop database") + logger.Logger.WithError(err).Error("Failed to stop database") return fmt.Errorf("failed to close database: %w", err) } @@ -83,7 +70,7 @@ func (bs *backupService) CreateBackup(unlockPassword string, w io.Writer) error 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") + logger.Logger.WithField("lnFiles", lnFiles).Info("Listed node storage dir") // Avoid backing up log files. slices.DeleteFunc(lnFiles, func(s string) bool { @@ -118,27 +105,27 @@ func (bs *backupService) CreateBackup(unlockPassword string, w io.Writer) error } // Locate the main database file. - dbFilePath := bs.svc.GetConfig().GetEnv().DatabaseUri + dbFilePath := api.cfg.GetEnv().DatabaseUri // Add the database file to the archive. - bs.logger.WithField("nwc.db", dbFilePath).Info("adding nwc db to zip") + logger.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") + logger.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") + logger.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") + logger.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") + logger.Logger.WithError(err).Error("Failed to write file to zip") return fmt.Errorf("failed to write input file to zip: %w", err) } } @@ -146,13 +133,13 @@ func (bs *backupService) CreateBackup(unlockPassword string, w io.Writer) error return nil } -func (bs *backupService) RestoreBackup(unlockPassword string, r io.Reader) error { - workDir, err := filepath.Abs(bs.svc.GetConfig().GetEnv().Workdir) +func (api *api) RestoreBackup(unlockPassword string, r io.Reader) error { + workDir, err := filepath.Abs(api.cfg.GetEnv().Workdir) if err != nil { return fmt.Errorf("failed to get absolute workdir: %w", err) } - if strings.HasPrefix(bs.svc.GetConfig().GetEnv().DatabaseUri, "file:") { + if strings.HasPrefix(api.cfg.GetEnv().DatabaseUri, "file:") { return errors.New("cannot restore backup when database path is a file URI") } @@ -213,17 +200,17 @@ func (bs *backupService) RestoreBackup(unlockPassword string, r io.Reader) error return nil } - bs.logger.WithField("count", len(zr.File)).Info("Extracting files") + logger.Logger.WithField("count", len(zr.File)).Info("Extracting files") for _, f := range zr.File { - bs.logger.WithField("file", f.Name).Info("Extracting file") + logger.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") + logger.Logger.WithField("count", len(zr.File)).Info("Extracted files") go func() { - bs.logger.Info("Backup restored. Shutting down Alby Hub...") + logger.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) diff --git a/api/esplora.go b/api/esplora.go index f1d185501..8dd0e31a4 100644 --- a/api/esplora.go +++ b/api/esplora.go @@ -8,11 +8,12 @@ import ( "net/http" "time" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/sirupsen/logrus" ) func (api *api) RequestEsploraApi(endpoint string) (interface{}, error) { - url := api.svc.GetConfig().GetEnv().LDKEsploraServer + endpoint + url := api.cfg.GetEnv().LDKEsploraServer + endpoint client := http.Client{ Timeout: time.Second * 10, @@ -20,7 +21,7 @@ func (api *api) RequestEsploraApi(endpoint string) (interface{}, error) { req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { - api.logger.WithError(err).WithFields(logrus.Fields{ + logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": url, }).Error("Failed to create http request") return nil, err @@ -28,7 +29,7 @@ func (api *api) RequestEsploraApi(endpoint string) (interface{}, error) { res, err := client.Do(req) if err != nil { - api.logger.WithError(err).WithFields(logrus.Fields{ + logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": url, }).Error("Failed to send request") return nil, err @@ -38,7 +39,7 @@ func (api *api) RequestEsploraApi(endpoint string) (interface{}, error) { body, readErr := io.ReadAll(res.Body) if readErr != nil { - api.logger.WithError(err).WithFields(logrus.Fields{ + logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": url, }).Error("Failed to read response body") return nil, errors.New("failed to read response body") @@ -47,7 +48,7 @@ func (api *api) RequestEsploraApi(endpoint string) (interface{}, error) { var jsonContent interface{} jsonErr := json.Unmarshal(body, &jsonContent) if jsonErr != nil { - api.logger.WithError(jsonErr).WithFields(logrus.Fields{ + logger.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)) diff --git a/lsp/lsp_service.go b/api/lsp.go similarity index 71% rename from lsp/lsp_service.go rename to api/lsp.go index 15479a5cb..6855326f5 100644 --- a/lsp/lsp_service.go +++ b/api/lsp.go @@ -1,4 +1,4 @@ -package lsp +package api import ( "bytes" @@ -14,15 +14,11 @@ import ( "time" "github.com/getAlby/nostr-wallet-connect/lnclient" - "github.com/getAlby/nostr-wallet-connect/service" + "github.com/getAlby/nostr-wallet-connect/logger" + "github.com/getAlby/nostr-wallet-connect/lsp" "github.com/sirupsen/logrus" ) -type lspService struct { - svc service.Service - logger *logrus.Logger -} - type lspInfo struct { Pubkey string Address string @@ -30,85 +26,78 @@ type lspInfo struct { MaxChannelExpiryBlocks uint64 } -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 +func (api *api) NewInstantChannelInvoice(ctx context.Context, request *NewInstantChannelInvoiceRequest) (*NewInstantChannelInvoiceResponse, error) { + var selectedLsp lsp.LSP switch request.LSP { case "VOLTAGE": - selectedLsp = VoltageLSP() + selectedLsp = lsp.VoltageLSP() case "OLYMPUS_FLOW_2_0": - selectedLsp = OlympusLSP() + selectedLsp = lsp.OlympusLSP() case "OLYMPUS_MUTINYNET_FLOW_2_0": - selectedLsp = OlympusMutinynetFlowLSP() + selectedLsp = lsp.OlympusMutinynetFlowLSP() case "OLYMPUS_MUTINYNET_LSPS1": - selectedLsp = OlympusMutinynetLSPS1LSP() + selectedLsp = lsp.OlympusMutinynetLSPS1LSP() case "ALBY": - selectedLsp = AlbyPlebsLSP() + selectedLsp = lsp.AlbyPlebsLSP() case "ALBY_MUTINYNET": - selectedLsp = AlbyMutinynetPlebsLSP() + selectedLsp = lsp.AlbyMutinynetPlebsLSP() case "MEGALITH": - selectedLsp = MegalithLSP() + selectedLsp = lsp.MegalithLSP() case "MEGALITH_MUTINYNET": - selectedLsp = MegalithMutinynetLSP() + selectedLsp = lsp.MegalithMutinynetLSP() default: return nil, errors.New("unknown LSP") } - if ls.svc.GetLNClient() == nil { + if api.svc.GetLNClient() == nil { return nil, errors.New("LNClient not started") } - if selectedLsp.LspType != LSP_TYPE_LSPS1 && request.Public { + if selectedLsp.LspType != lsp.LSP_TYPE_LSPS1 && request.Public { return nil, errors.New("This LSP option does not support public channels") } - ls.logger.Infoln("Requesting LSP info") + logger.Logger.Infoln("Requesting LSP info") var lspInfo *lspInfo var err error switch selectedLsp.LspType { - case LSP_TYPE_FLOW_2_0: + case lsp.LSP_TYPE_FLOW_2_0: fallthrough - case LSP_TYPE_PMLSP: - lspInfo, err = ls.getFlowLSPInfo(selectedLsp.Url + "/info") + case lsp.LSP_TYPE_PMLSP: + lspInfo, err = api.getFlowLSPInfo(selectedLsp.Url + "/info") - case LSP_TYPE_LSPS1: - lspInfo, err = ls.getLSPS1LSPInfo(selectedLsp.Url + "/get_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 { - ls.logger.WithError(err).Error("Failed to request LSP info") + logger.Logger.WithError(err).Error("Failed to request LSP info") return nil, err } - ls.logger.Infoln("Requesting own node info") + logger.Logger.Infoln("Requesting own node info") - nodeInfo, err := ls.svc.GetLNClient().GetInfo(ctx) + nodeInfo, err := api.svc.GetLNClient().GetInfo(ctx) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.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") + logger.Logger.WithField("lspInfo", lspInfo).Info("Connecting to LSP node as a peer") - err = ls.svc.GetLNClient().ConnectPeer(ctx, &lnclient.ConnectPeerRequest{ + err = api.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") + logger.Logger.WithError(err).Error("Failed to connect to peer") return nil, err } @@ -116,18 +105,18 @@ func (ls *lspService) NewInstantChannelInvoice(ctx context.Context, request *New 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, request.Public, lspInfo.MaxChannelExpiryBlocks) + 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(ctx, &selectedLsp, request.Amount, nodeInfo.Pubkey, request.Public, lspInfo.MaxChannelExpiryBlocks) default: return nil, fmt.Errorf("unsupported LSP type: %v", selectedLsp.LspType) } if err != nil { - ls.logger.WithError(err).Error("Failed to request invoice") + logger.Logger.WithError(err).Error("Failed to request invoice") return nil, err } @@ -136,14 +125,14 @@ func (ls *lspService) NewInstantChannelInvoice(ctx context.Context, request *New Fee: fee, } - ls.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "newChannelResponse": newChannelResponse, }).Info("New Channel response") return newChannelResponse, nil } -func (ls *lspService) getLSPS1LSPInfo(url string) (*lspInfo, error) { +func (api *api) getLSPS1LSPInfo(url string) (*lspInfo, error) { type lsps1LSPInfoOptions struct { MaxChannelExpiryBlocks uint64 `json:"max_channel_expiry_blocks"` @@ -158,7 +147,7 @@ func (ls *lspService) getLSPS1LSPInfo(url string) (*lspInfo, error) { } req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": url, }).Error("Failed to create lsp info request") return nil, err @@ -166,7 +155,7 @@ func (ls *lspService) getLSPS1LSPInfo(url string) (*lspInfo, error) { res, err := client.Do(req) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": url, }).Error("Failed to request lsp info") return nil, err @@ -176,7 +165,7 @@ func (ls *lspService) getLSPS1LSPInfo(url string) (*lspInfo, error) { body, err := io.ReadAll(res.Body) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": url, }).Error("Failed to read response body") return nil, errors.New("failed to read response body") @@ -184,7 +173,7 @@ func (ls *lspService) getLSPS1LSPInfo(url string) (*lspInfo, error) { err = json.Unmarshal(body, &lsps1LspInfo) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.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)) @@ -195,15 +184,15 @@ func (ls *lspService) getLSPS1LSPInfo(url string) (*lspInfo, error) { // 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") + logger.Logger.WithField("parts", parts).Info("Split URI") if parts == nil || len(parts) != 4 { - ls.logger.WithField("parts", parts).Info("Unsupported URI") + logger.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") + logger.Logger.WithField("port", parts[3]).WithError(err).Info("Failed to decode port number") return nil, err } @@ -215,7 +204,7 @@ func (ls *lspService) getLSPS1LSPInfo(url string) (*lspInfo, error) { MaxChannelExpiryBlocks: lsps1LspInfo.Options.MaxChannelExpiryBlocks, }, nil } -func (ls *lspService) getFlowLSPInfo(url string) (*lspInfo, error) { +func (api *api) getFlowLSPInfo(url string) (*lspInfo, error) { type FlowLSPConnectionMethod struct { Address string `json:"address"` Port uint16 `json:"port"` @@ -231,7 +220,7 @@ func (ls *lspService) getFlowLSPInfo(url string) (*lspInfo, error) { } req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": url, }).Error("Failed to create lsp info request") return nil, err @@ -239,7 +228,7 @@ func (ls *lspService) getFlowLSPInfo(url string) (*lspInfo, error) { res, err := client.Do(req) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": url, }).Error("Failed to request lsp info") return nil, err @@ -249,7 +238,7 @@ func (ls *lspService) getFlowLSPInfo(url string) (*lspInfo, error) { body, err := io.ReadAll(res.Body) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": url, }).Error("Failed to read response body") return nil, errors.New("failed to read response body") @@ -257,7 +246,7 @@ func (ls *lspService) getFlowLSPInfo(url string) (*lspInfo, error) { err = json.Unmarshal(body, &flowLspInfo) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.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)) @@ -272,7 +261,7 @@ func (ls *lspService) getFlowLSPInfo(url string) (*lspInfo, error) { } if ipIndex == -1 { - ls.logger.Error("No ipv4/ipv6 connection method found in LSP info") + logger.Logger.Error("No ipv4/ipv6 connection method found in LSP info") return nil, errors.New("unexpected LSP connection method") } @@ -283,8 +272,8 @@ func (ls *lspService) getFlowLSPInfo(url string) (*lspInfo, error) { }, 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") +func (api *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *lsp.LSP, amount uint64, pubkey string) (invoice string, fee uint64, err error) { + logger.Logger.Infoln("Requesting fee information") type FeeRequest struct { AmountMsat uint64 `json:"amount_msat"` @@ -311,7 +300,7 @@ func (ls *lspService) requestFlow20WrappedInvoice(ctx context.Context, selectedL req, err := http.NewRequest(http.MethodPost, selectedLsp.Url+"/fee", bodyReader) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": selectedLsp.Url, }).Error("Failed to create lsp fee request") return "", 0, err @@ -321,7 +310,7 @@ func (ls *lspService) requestFlow20WrappedInvoice(ctx context.Context, selectedL res, err := client.Do(req) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": selectedLsp.Url, }).Error("Failed to request lsp fee") return "", 0, err @@ -331,14 +320,14 @@ func (ls *lspService) requestFlow20WrappedInvoice(ctx context.Context, selectedL body, err := io.ReadAll(res.Body) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.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{ + logger.Logger.WithFields(logrus.Fields{ "body": string(body), "statusCode": res.StatusCode, }).Error("fee endpoint returned non-success code") @@ -347,18 +336,18 @@ func (ls *lspService) requestFlow20WrappedInvoice(ctx context.Context, selectedL err = json.Unmarshal(body, &feeResponse) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.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{ + logger.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{ + logger.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) @@ -368,9 +357,9 @@ func (ls *lspService) requestFlow20WrappedInvoice(ctx context.Context, selectedL // 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) + makeInvoiceResponse, err := api.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") + logger.Logger.WithError(err).Error("Failed to request own invoice") return "", 0, fmt.Errorf("failed to request own invoice %v", err) } @@ -382,7 +371,7 @@ func (ls *lspService) requestFlow20WrappedInvoice(ctx context.Context, selectedL Bolt11 string `json:"jit_bolt11"` } - ls.logger.Infoln("Proposing invoice") + logger.Logger.Infoln("Proposing invoice") var proposalResponse ProposalResponse { @@ -400,7 +389,7 @@ func (ls *lspService) requestFlow20WrappedInvoice(ctx context.Context, selectedL req, err := http.NewRequest(http.MethodPost, selectedLsp.Url+"/proposal", bodyReader) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": selectedLsp.Url, }).Error("Failed to create lsp fee request") return "", 0, err @@ -410,7 +399,7 @@ func (ls *lspService) requestFlow20WrappedInvoice(ctx context.Context, selectedL res, err := client.Do(req) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": selectedLsp.Url, }).Error("Failed to request lsp fee") return "", 0, err @@ -420,14 +409,14 @@ func (ls *lspService) requestFlow20WrappedInvoice(ctx context.Context, selectedL body, err := io.ReadAll(res.Body) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.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{ + logger.Logger.WithFields(logrus.Fields{ "body": string(body), "statusCode": res.StatusCode, }).Error("proposal endpoint returned non-success code") @@ -436,14 +425,14 @@ func (ls *lspService) requestFlow20WrappedInvoice(ctx context.Context, selectedL err = json.Unmarshal(body, &proposalResponse) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.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") + logger.Logger.WithField("proposalResponse", proposalResponse).Info("Got proposal response") if proposalResponse.Bolt11 == "" { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": selectedLsp.Url, "proposalResponse": proposalResponse, }).Error("No bolt11 in proposal response") @@ -455,7 +444,7 @@ func (ls *lspService) requestFlow20WrappedInvoice(ctx context.Context, selectedL return invoice, fee, nil } -func (ls *lspService) requestPMLSPInvoice(selectedLsp *LSP, amount uint64, pubkey string) (invoice string, fee uint64, err error) { +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"` @@ -475,7 +464,7 @@ func (ls *lspService) requestPMLSPInvoice(selectedLsp *LSP, amount uint64, pubke req, err := http.NewRequest(http.MethodPost, selectedLsp.Url+"/new-channel", bodyReader) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": selectedLsp.Url, }).Error("Failed to create new channel request") return "", 0, err @@ -485,7 +474,7 @@ func (ls *lspService) requestPMLSPInvoice(selectedLsp *LSP, amount uint64, pubke res, err := client.Do(req) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": selectedLsp.Url, }).Error("Failed to request new channel invoice") return "", 0, err @@ -495,25 +484,30 @@ func (ls *lspService) requestPMLSPInvoice(selectedLsp *LSP, amount uint64, pubke body, err := io.ReadAll(res.Body) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.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{ + logger.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 + type newInstantChannelResponse struct { + FeeAmountMsat uint64 `json:"fee_amount_msat"` + Invoice string `json:"invoice"` + } + + var newChannelResponse newInstantChannelResponse err = json.Unmarshal(body, &newChannelResponse) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.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)) @@ -525,7 +519,7 @@ func (ls *lspService) requestPMLSPInvoice(selectedLsp *LSP, amount uint64, pubke return invoice, fee, nil } -func (ls *lspService) requestLSPS1Invoice(ctx context.Context, selectedLsp *LSP, amount uint64, pubkey string, public bool, channelExpiryBlocks uint64) (invoice string, fee uint64, err error) { +func (api *api) requestLSPS1Invoice(ctx context.Context, selectedLsp *lsp.LSP, amount uint64, pubkey string, public bool, channelExpiryBlocks uint64) (invoice string, fee uint64, err error) { client := http.Client{ Timeout: time.Second * 10, } @@ -542,9 +536,9 @@ func (ls *lspService) requestLSPS1Invoice(ctx context.Context, selectedLsp *LSP, AnnounceChannel bool `json:"announce_channel"` } - refundAddress, err := ls.svc.GetLNClient().GetNewOnchainAddress(ctx) + refundAddress, err := api.svc.GetLNClient().GetNewOnchainAddress(ctx) if err != nil { - ls.logger.WithError(err).Error("Failed to request onchain address") + logger.Logger.WithError(err).Error("Failed to request onchain address") return "", 0, err } @@ -576,7 +570,7 @@ func (ls *lspService) requestLSPS1Invoice(ctx context.Context, selectedLsp *LSP, req, err := http.NewRequest(http.MethodPost, selectedLsp.Url+"/create_order", bodyReader) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": selectedLsp.Url, }).Error("Failed to create new channel request") return "", 0, err @@ -586,7 +580,7 @@ func (ls *lspService) requestLSPS1Invoice(ctx context.Context, selectedLsp *LSP, res, err := client.Do(req) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": selectedLsp.Url, }).Error("Failed to request new channel invoice") return "", 0, err @@ -596,14 +590,14 @@ func (ls *lspService) requestLSPS1Invoice(ctx context.Context, selectedLsp *LSP, body, err := io.ReadAll(res.Body) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.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{ + logger.Logger.WithFields(logrus.Fields{ "newLSPS1ChannelRequest": newLSPS1ChannelRequest, "body": string(body), "statusCode": res.StatusCode, @@ -623,7 +617,7 @@ func (ls *lspService) requestLSPS1Invoice(ctx context.Context, selectedLsp *LSP, err = json.Unmarshal(body, &newChannelResponse) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.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)) @@ -632,7 +626,7 @@ func (ls *lspService) requestLSPS1Invoice(ctx context.Context, selectedLsp *LSP, invoice = newChannelResponse.Payment.Bolt11Invoice fee, err = strconv.ParseUint(newChannelResponse.Payment.FeeTotalSat, 10, 64) if err != nil { - ls.logger.WithError(err).WithFields(logrus.Fields{ + logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": selectedLsp.Url, }).Error("Failed to parse fee") return "", 0, fmt.Errorf("failed to parse fee %v", err) diff --git a/api/models.go b/api/models.go index 1f912721f..f42a6126c 100644 --- a/api/models.go +++ b/api/models.go @@ -2,13 +2,12 @@ package api import ( "context" + "io" "time" "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 { @@ -45,8 +44,9 @@ type API interface { GetNetworkGraph(nodeIds []string) (NetworkGraphResponse, error) SyncWallet() error GetLogOutput(ctx context.Context, logType string, getLogRequest *GetLogOutputRequest) (*GetLogOutputResponse, error) - GetLSPService() lsp.LSPService - GetBackupService() backup.BackupService + NewInstantChannelInvoice(ctx context.Context, request *NewInstantChannelInvoiceRequest) (*NewInstantChannelInvoiceResponse, error) + CreateBackup(unlockPassword string, w io.Writer) error + RestoreBackup(unlockPassword string, r io.Reader) error } type App struct { @@ -60,8 +60,8 @@ type App struct { LastEventAt *time.Time `json:"lastEventAt"` ExpiresAt *time.Time `json:"expiresAt"` RequestMethods []string `json:"requestMethods"` - MaxAmount int `json:"maxAmount"` - BudgetUsage int64 `json:"budgetUsage"` + MaxAmount uint64 `json:"maxAmount"` + BudgetUsage uint64 `json:"budgetUsage"` BudgetRenewal string `json:"budgetRenewal"` } @@ -70,7 +70,7 @@ type ListAppsResponse struct { } type UpdateAppRequest struct { - MaxAmount int `json:"maxAmount"` + MaxAmount uint64 `json:"maxAmount"` BudgetRenewal string `json:"budgetRenewal"` ExpiresAt string `json:"expiresAt"` RequestMethods string `json:"requestMethods"` @@ -79,7 +79,7 @@ type UpdateAppRequest struct { type CreateAppRequest struct { Name string `json:"name"` Pubkey string `json:"pubkey"` - MaxAmount int `json:"maxAmount"` + MaxAmount uint64 `json:"maxAmount"` BudgetRenewal string `json:"budgetRenewal"` ExpiresAt string `json:"expiresAt"` RequestMethods string `json:"requestMethods"` @@ -228,3 +228,14 @@ type BasicRestoreWailsRequest struct { } type NetworkGraphResponse = lnclient.NetworkGraphResponse + +type NewInstantChannelInvoiceRequest struct { + Amount uint64 `json:"amount"` + LSP string `json:"lsp"` + Public bool `json:"public"` +} + +type NewInstantChannelInvoiceResponse struct { + Invoice string `json:"invoice"` + Fee uint64 `json:"fee"` +} diff --git a/backup/models.go b/backup/models.go deleted file mode 100644 index c2ad02d2f..000000000 --- a/backup/models.go +++ /dev/null @@ -1,10 +0,0 @@ -package backup - -import ( - "io" -) - -type BackupService interface { - CreateBackup(unlockPassword string, w io.Writer) error - RestoreBackup(unlockPassword string, r io.Reader) error -} diff --git a/cmd/http/main.go b/cmd/http/main.go new file mode 100644 index 000000000..7a40fa0bd --- /dev/null +++ b/cmd/http/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "context" + "fmt" + nethttp "net/http" + "os" + "os/signal" + "syscall" + "time" + + echologrus "github.com/davrux/echo-logrus/v4" + "github.com/getAlby/nostr-wallet-connect/http" + "github.com/getAlby/nostr-wallet-connect/logger" + "github.com/getAlby/nostr-wallet-connect/service" + "github.com/labstack/echo/v4" + log "github.com/sirupsen/logrus" +) + +func main() { + log.Info("NWC Starting in HTTP mode") + ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, os.Kill) + svc, _ := service.NewService(ctx) + + echologrus.Logger = logger.Logger + e := echo.New() + + //register shared routes + httpSvc := http.NewHttpService(svc, svc.GetEventPublisher()) + httpSvc.RegisterSharedRoutes(e) + //start Echo server + go func() { + if err := e.Start(fmt.Sprintf(":%v", svc.GetConfig().GetEnv().Port)); err != nil && err != nethttp.ErrServerClosed { + logger.Logger.Fatalf("shutting down the server: %v", err) + } + }() + //handle graceful shutdown + <-ctx.Done() + logger.Logger.Infof("Shutting down echo server...") + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + e.Shutdown(ctx) + logger.Logger.Info("Echo server exited") + svc.WaitShutdown() + logger.Logger.Info("Service exited") +} diff --git a/config/config.go b/config/config.go index 029a62494..0fad5cd46 100644 --- a/config/config.go +++ b/config/config.go @@ -7,37 +7,32 @@ import ( "fmt" "os" - "github.com/nbd-wtf/go-nostr" - "github.com/sirupsen/logrus" + "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/logger" "gorm.io/gorm" "gorm.io/gorm/clause" - - dbModels "github.com/getAlby/nostr-wallet-connect/db" ) type config struct { - Env *AppConfig - CookieSecret string - NostrSecretKey string - NostrPublicKey string - db *gorm.DB - logger *logrus.Logger + Env *AppConfig + CookieSecret string + db *gorm.DB } const ( unlockPasswordCheck = "THIS STRING SHOULD MATCH IF PASSWORD IS CORRECT" ) -func NewConfig(db *gorm.DB, env *AppConfig, logger *logrus.Logger) *config { - cfg := &config{} - cfg.init(db, env, logger) +func NewConfig(env *AppConfig, db *gorm.DB) *config { + cfg := &config{ + db: db, + } + cfg.init(env) return cfg } -func (cfg *config) init(db *gorm.DB, env *AppConfig, logger *logrus.Logger) { - cfg.db = db +func (cfg *config) init(env *AppConfig) { cfg.Env = env - cfg.logger = logger if cfg.Env.Relay != "" { cfg.SetUpdate("Relay", cfg.Env.Relay, "") @@ -53,7 +48,7 @@ func (cfg *config) init(db *gorm.DB, env *AppConfig, logger *logrus.Logger) { if cfg.Env.LNDCertFile != "" { certBytes, err := os.ReadFile(cfg.Env.LNDCertFile) if err != nil { - logger.Fatalf("Failed to read LND cert file: %v", err) + logger.Logger.Fatalf("Failed to read LND cert file: %v", err) } certHex := hex.EncodeToString(certBytes) cfg.SetUpdate("LNDCertHex", certHex, "") @@ -61,7 +56,7 @@ func (cfg *config) init(db *gorm.DB, env *AppConfig, logger *logrus.Logger) { if cfg.Env.LNDMacaroonFile != "" { macBytes, err := os.ReadFile(cfg.Env.LNDMacaroonFile) if err != nil { - logger.Fatalf("Failed to read LND macaroon file: %v", err) + logger.Logger.Fatalf("Failed to read LND macaroon file: %v", err) } macHex := hex.EncodeToString(macBytes) cfg.SetUpdate("LNDMacaroonHex", macHex, "") @@ -86,14 +81,6 @@ func (cfg *config) init(db *gorm.DB, env *AppConfig, logger *logrus.Logger) { } } -func (cfg *config) GetNostrPublicKey() string { - return cfg.NostrPublicKey -} - -func (cfg *config) GetNostrSecretKey() string { - return cfg.NostrSecretKey -} - func (cfg *config) GetCookieSecret() string { return cfg.CookieSecret } @@ -107,11 +94,11 @@ 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) { - var userConfig dbModels.UserConfig - err := db.Where(&dbModels.UserConfig{Key: key}).Limit(1).Find(&userConfig).Error +func (cfg *config) get(key string, encryptionKey string, gormDB *gorm.DB) (string, error) { + var userConfig db.UserConfig + err := gormDB.Where(&db.UserConfig{Key: key}).Limit(1).Find(&userConfig).Error if err != nil { - return "", fmt.Errorf("failed to get configuration value: %w", db.Error) + return "", fmt.Errorf("failed to get configuration value: %w", gormDB.Error) } value := userConfig.Value @@ -125,7 +112,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, gormDB *gorm.DB) error { if encryptionKey != "" { encrypted, err := AesGcmEncrypt(value, encryptionKey) if err != nil { @@ -133,8 +120,8 @@ func (cfg *config) set(key string, value string, clauses clause.OnConflict, encr } value = encrypted } - userConfig := dbModels.UserConfig{Key: key, Value: value, Encrypted: encryptionKey != ""} - result := db.Clauses(clauses).Create(&userConfig) + userConfig := db.UserConfig{Key: key, Value: value, Encrypted: encryptionKey != ""} + result := gormDB.Clauses(clauses).Create(&userConfig) if result.Error != nil { return fmt.Errorf("failed to save key to config: %v", result.Error) @@ -149,7 +136,7 @@ func (cfg *config) SetIgnore(key string, value string, encryptionKey string) { } err := cfg.set(key, value, clauses, encryptionKey, cfg.db) if err != nil { - cfg.logger.Fatalf("Failed to save config: %v", err) + logger.Logger.Fatalf("Failed to save config: %v", err) } } @@ -160,7 +147,7 @@ func (cfg *config) SetUpdate(key string, value string, encryptionKey string) { } err := cfg.set(key, value, clauses, encryptionKey, cfg.db) if err != nil { - cfg.logger.Fatalf("Failed to save config: %v", err) + logger.Logger.Fatalf("Failed to save config: %v", err) } } @@ -170,18 +157,18 @@ func (cfg *config) ChangeUnlockPassword(currentUnlockPassword string, newUnlockP } err := cfg.db.Transaction(func(tx *gorm.DB) error { - var encryptedUserConfigs []dbModels.UserConfig - err := tx.Where(&dbModels.UserConfig{Encrypted: true}).Find(&encryptedUserConfigs).Error + var encryptedUserConfigs []db.UserConfig + err := tx.Where(&db.UserConfig{Encrypted: true}).Find(&encryptedUserConfigs).Error if err != nil { return err } - cfg.logger.WithField("count", len(encryptedUserConfigs)).Info("Updating encrypted entries") + logger.Logger.WithField("count", len(encryptedUserConfigs)).Info("Updating encrypted entries") for _, userConfig := range encryptedUserConfigs { decryptedValue, err := cfg.get(userConfig.Key, currentUnlockPassword, tx) if err != nil { - cfg.logger.WithField("key", userConfig.Key).WithError(err).Error("Failed to decrypt key") + logger.Logger.WithField("key", userConfig.Key).WithError(err).Error("Failed to decrypt key") return err } clauses := clause.OnConflict{ @@ -190,10 +177,10 @@ func (cfg *config) ChangeUnlockPassword(currentUnlockPassword string, newUnlockP } err = cfg.set(userConfig.Key, decryptedValue, clauses, newUnlockPassword, tx) if err != nil { - cfg.logger.WithField("key", userConfig.Key).WithError(err).Error("Failed to encrypt key") + logger.Logger.WithField("key", userConfig.Key).WithError(err).Error("Failed to encrypt key") return err } - cfg.logger.WithField("key", userConfig.Key).Info("re-encrypted key") + logger.Logger.WithField("key", userConfig.Key).Info("re-encrypted key") } // commit transaction @@ -201,7 +188,7 @@ func (cfg *config) ChangeUnlockPassword(currentUnlockPassword string, newUnlockP }) if err != nil { - cfg.logger.WithError(err).Error("failed to execute db transaction") + logger.Logger.WithError(err).Error("failed to execute db transaction") return err } @@ -214,27 +201,11 @@ func (cfg *config) CheckUnlockPassword(encryptionKey string) bool { return err == nil && (decryptedValue == "" || decryptedValue == unlockPasswordCheck) } +// TODO: rename 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 } diff --git a/config/models.go b/config/models.go index dd397b658..3c8cb0ac4 100644 --- a/config/models.go +++ b/config/models.go @@ -50,13 +50,10 @@ type Config interface { Get(key string, encryptionKey string) (string, error) 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.go b/db/db.go new file mode 100644 index 000000000..d6cbbbb3a --- /dev/null +++ b/db/db.go @@ -0,0 +1,54 @@ +package db + +import ( + "fmt" + + "github.com/getAlby/nostr-wallet-connect/db/migrations" + "github.com/getAlby/nostr-wallet-connect/logger" + "github.com/glebarez/sqlite" + "gorm.io/gorm" +) + +func NewDB(uri string) (*gorm.DB, error) { + // var sqlDb *sql.DB + gormDB, err := gorm.Open(sqlite.Open(uri), &gorm.Config{}) + if err != nil { + return nil, err + } + err = gormDB.Exec("PRAGMA foreign_keys=ON;").Error + if err != nil { + return nil, err + } + err = gormDB.Exec("PRAGMA auto_vacuum=FULL;").Error + if err != nil { + return nil, err + } + + // sqlDb, err = DB.DB() + // if err != nil { + // return err + // } + // this causes errors when concurrently saving DB entries and otherwise requires mutexes + // sqlDb.SetMaxOpenConns(1) + + err = migrations.Migrate(gormDB) + if err != nil { + logger.Logger.WithError(err).Error("Failed to migrate") + return nil, err + } + + return gormDB, nil +} + +func Stop(db *gorm.DB) error { + sqlDB, err := db.DB() + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + + err = sqlDB.Close() + if err != nil { + return fmt.Errorf("failed to close database connection: %w", err) + } + return nil +} diff --git a/db/db_service.go b/db/db_service.go index 835ccb120..136895dc5 100644 --- a/db/db_service.go +++ b/db/db_service.go @@ -3,28 +3,24 @@ package db import ( "encoding/hex" "fmt" - "strings" "time" - "github.com/getAlby/nostr-wallet-connect/nip47" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/nbd-wtf/go-nostr" - "github.com/sirupsen/logrus" "gorm.io/gorm" ) type dbService struct { - db *gorm.DB - logger *logrus.Logger + db *gorm.DB } -func NewDBService(db *gorm.DB, logger *logrus.Logger) *dbService { +func NewDBService(db *gorm.DB) *dbService { return &dbService{ - db: db, - logger: logger, + db: db, } } -func (dbSvc *dbService) CreateApp(name string, pubkey string, maxAmount int, budgetRenewal string, expiresAt *time.Time, requestMethods []string) (*App, string, error) { +func (svc *dbService) CreateApp(name string, pubkey string, maxAmount uint64, budgetRenewal string, expiresAt *time.Time, requestMethods []string) (*App, string, error) { var pairingPublicKey string var pairingSecretKey string if pubkey == "" { @@ -35,30 +31,26 @@ func (dbSvc *dbService) CreateApp(name string, pubkey string, maxAmount int, bud //validate public key decoded, err := hex.DecodeString(pairingPublicKey) if err != nil || len(decoded) != 32 { - dbSvc.logger.WithField("pairingPublicKey", pairingPublicKey).Error("Invalid public key format") + logger.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 := svc.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, + MaxAmount: int(maxAmount), BudgetRenewal: budgetRenewal, } err = tx.Create(&appPermission).Error @@ -71,7 +63,7 @@ func (dbSvc *dbService) CreateApp(name string, pubkey string, maxAmount int, bud }) if err != nil { - dbSvc.logger.WithError(err).Error("Failed to save app") + logger.Logger.WithError(err).Error("Failed to save app") return nil, "", err } diff --git a/migrations/202401191539_initial_migration.go b/db/migrations/202401191539_initial_migration.go similarity index 100% rename from migrations/202401191539_initial_migration.go rename to db/migrations/202401191539_initial_migration.go diff --git a/migrations/202401191539_initial_migration.sql b/db/migrations/202401191539_initial_migration.sql similarity index 100% rename from migrations/202401191539_initial_migration.sql rename to db/migrations/202401191539_initial_migration.sql diff --git a/db/migrations/202403171120_delete_ldk_payments.go b/db/migrations/202403171120_delete_ldk_payments.go new file mode 100644 index 000000000..c4dcf9387 --- /dev/null +++ b/db/migrations/202403171120_delete_ldk_payments.go @@ -0,0 +1,45 @@ +package migrations + +import ( + _ "embed" + + "github.com/go-gormigrate/gormigrate/v2" + "gorm.io/gorm" +) + +// TODO: delete the whole migration +// Delete LDK payments that were not migrated to the new LDK format (PaymentKind) +// this has now been removed as it only affects old test builds that should have been updated by now +// in case someone encounters it, they can run the commands on their DB to fix the issue. +var _202403171120_delete_ldk_payments = &gormigrate.Migration{ + ID: "202403171120_delete_ldk_payments", + Migrate: func(tx *gorm.DB) error { + + /*ldkDbPath := filepath.Join(appConfig.Workdir, "ldk", "storage", "ldk_node_data.sqlite") + if _, err := os.Stat(ldkDbPath); errors.Is(err, os.ErrNotExist) { + logger.Logger.Info("No LDK database, skipping migration") + return nil + } + ldkDb, err := sql.Open("sqlite", ldkDbPath) + if err != nil { + return err + } + result, err := ldkDb.Exec(`update ldk_node_data set primary_namespace="payments_bkp" where primary_namespace == "payments"`) + if err != nil { + return err + } + rowsAffected, err := result.RowsAffected() + if err != nil { + return err + } + logger.Logger.WithFields(logrus.Fields{ + "rowsAffected": rowsAffected, + }).Info("Removed incompatible payments from LDK database") + + return err*/ + return nil + }, + Rollback: func(tx *gorm.DB) error { + return nil + }, +} diff --git a/migrations/202404021909_nullable_expires_at.go b/db/migrations/202404021909_nullable_expires_at.go similarity index 100% rename from migrations/202404021909_nullable_expires_at.go rename to db/migrations/202404021909_nullable_expires_at.go diff --git a/migrations/202405302121_store_decrypted_request.go b/db/migrations/202405302121_store_decrypted_request.go similarity index 100% rename from migrations/202405302121_store_decrypted_request.go rename to db/migrations/202405302121_store_decrypted_request.go diff --git a/migrations/202406061259_delete_content.go b/db/migrations/202406061259_delete_content.go similarity index 100% rename from migrations/202406061259_delete_content.go rename to db/migrations/202406061259_delete_content.go diff --git a/migrations/202406071726_vacuum.go b/db/migrations/202406071726_vacuum.go similarity index 100% rename from migrations/202406071726_vacuum.go rename to db/migrations/202406071726_vacuum.go diff --git a/migrations/README.md b/db/migrations/README.md similarity index 100% rename from migrations/README.md rename to db/migrations/README.md diff --git a/db/migrations/migrate.go b/db/migrations/migrate.go new file mode 100644 index 000000000..cdd3f6b97 --- /dev/null +++ b/db/migrations/migrate.go @@ -0,0 +1,20 @@ +package migrations + +import ( + "github.com/go-gormigrate/gormigrate/v2" + "gorm.io/gorm" +) + +func Migrate(gormDB *gorm.DB) error { + + m := gormigrate.New(gormDB, gormigrate.DefaultOptions, []*gormigrate.Migration{ + _202401191539_initial_migration, + _202403171120_delete_ldk_payments, + _202404021909_nullable_expires_at, + _202405302121_store_decrypted_request, + _202406061259_delete_content, + _202406071726_vacuum, + }) + + return m.Migrate() +} diff --git a/db/models.go b/db/models.go index 516dc0f9c..23a75e869 100644 --- a/db/models.go +++ b/db/models.go @@ -68,7 +68,7 @@ type Payment struct { } type DBService interface { - CreateApp(name string, pubkey string, maxAmount int, budgetRenewal string, expiresAt *time.Time, requestMethods []string) (*App, string, error) + CreateApp(name string, pubkey string, maxAmount uint64, budgetRenewal string, expiresAt *time.Time, requestMethods []string) (*App, string, error) } const ( diff --git a/events/events.go b/events/events.go index dab2ee5c1..ac7ff3c76 100644 --- a/events/events.go +++ b/events/events.go @@ -5,19 +5,18 @@ import ( "slices" "sync" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/sirupsen/logrus" ) type eventPublisher struct { - logger *logrus.Logger listeners []EventSubscriber subscriberMtx sync.Mutex globalProperties map[string]interface{} } -func NewEventPublisher(logger *logrus.Logger) *eventPublisher { +func NewEventPublisher() *eventPublisher { eventPublisher := &eventPublisher{ - logger: logger, listeners: []EventSubscriber{}, globalProperties: map[string]interface{}{}, } @@ -44,15 +43,15 @@ func (el *eventPublisher) RemoveSubscriber(listenerToRemove EventSubscriber) { } } -func (el *eventPublisher) Publish(event *Event) { - el.subscriberMtx.Lock() - defer el.subscriberMtx.Unlock() - el.logger.WithFields(logrus.Fields{"event": event}).Info("Logging event") - for _, listener := range el.listeners { +func (ep *eventPublisher) Publish(event *Event) { + ep.subscriberMtx.Lock() + defer ep.subscriberMtx.Unlock() + logger.Logger.WithFields(logrus.Fields{"event": event}).Info("Logging event") + for _, listener := range ep.listeners { go func(listener EventSubscriber) { - err := listener.ConsumeEvent(context.Background(), event, el.globalProperties) + err := listener.ConsumeEvent(context.Background(), event, ep.globalProperties) if err != nil { - el.logger.WithError(err).Error("Failed to consume event") + logger.Logger.WithError(err).Error("Failed to consume event") } }(listener) } diff --git a/frontend/platform_specific/wails/src/utils/request.ts b/frontend/platform_specific/wails/src/utils/request.ts index 14d10514c..a91358a0f 100644 --- a/frontend/platform_specific/wails/src/utils/request.ts +++ b/frontend/platform_specific/wails/src/utils/request.ts @@ -1,4 +1,4 @@ -import { WailsRequestRouter } from "wailsjs/go/main/WailsApp"; +import { WailsRequestRouter } from "wailsjs/go/wails/WailsApp"; export const request = async ( ...args: Parameters diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 63434df7b..d1b03f9d7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,11 +6,11 @@ import { usePosthog } from "./hooks/usePosthog"; import { Toaster } from "src/components/ui/toaster"; import routes from "src/routes.tsx"; +const router = createHashRouter(routes); + function App() { usePosthog(); - const router = createHashRouter(routes); - return ( <> diff --git a/frontend/src/components/layouts/AppLayout.tsx b/frontend/src/components/layouts/AppLayout.tsx index 260a06e8b..50d6fdc65 100644 --- a/frontend/src/components/layouts/AppLayout.tsx +++ b/frontend/src/components/layouts/AppLayout.tsx @@ -73,6 +73,8 @@ export default function AppLayout() { toast({ title: "You are now logged out." }); }, [csrf, navigate, refetchInfo, toast]); + const isHttpMode = window.location.protocol.startsWith("http"); + function UserMenuContent() { return ( @@ -88,13 +90,15 @@ export default function AppLayout() { - - -

Lock Alby Hub

-
+ {isHttpMode && ( + + +

Lock Alby Hub

+
+ )}
); } diff --git a/handle_balance_request.go b/handle_balance_request.go deleted file mode 100644 index dd85d33f9..000000000 --- a/handle_balance_request.go +++ /dev/null @@ -1,62 +0,0 @@ -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" -) - -const ( - MSAT_PER_SAT = 1000 -) - -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 { - publishResponse(resp, nostr.Tags{}) - return - } - - 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{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - }).Infof("Failed to fetch balance: %v", err) - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, - Message: err.Error(), - }, - }, nostr.Tags{}) - return - } - - responsePayload := &nip47.BalanceResponse{ - Balance: balance, - } - - appPermission := db.AppPermission{} - svc.db.Where("app_id = ? AND request_method = ?", app.ID, nip47.PAY_INVOICE_METHOD).First(&appPermission) - - maxAmount := appPermission.MaxAmount - if maxAmount > 0 { - responsePayload.MaxAmount = maxAmount * MSAT_PER_SAT - responsePayload.BudgetRenewal = appPermission.BudgetRenewal - } - - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Result: responsePayload, - }, nostr.Tags{}) -} diff --git a/handle_info_request.go b/handle_info_request.go deleted file mode 100644 index f2f610ed7..000000000 --- a/handle_info_request.go +++ /dev/null @@ -1,62 +0,0 @@ -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 *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 { - publishResponse(resp, nostr.Tags{}) - return - } - - 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{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - }).Infof("Failed to fetch node info: %v", err) - - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, - Message: err.Error(), - }, - }, nostr.Tags{}) - return - } - - network := info.Network - // Some implementations return "bitcoin" while NIP47 expects "mainnet" - if network == "bitcoin" { - network = "mainnet" - } - - responsePayload := &nip47.GetInfoResponse{ - Alias: info.Alias, - Color: info.Color, - Pubkey: info.Pubkey, - Network: network, - BlockHeight: info.BlockHeight, - BlockHash: info.BlockHash, - Methods: svc.GetMethods(app), - } - - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Result: responsePayload, - }, nostr.Tags{}) -} diff --git a/handle_list_transactions_request.go b/handle_list_transactions_request.go deleted file mode 100644 index fa883752f..000000000 --- a/handle_list_transactions_request.go +++ /dev/null @@ -1,65 +0,0 @@ -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 *nip47.Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*nip47.Response, nostr.Tags)) { - - listParams := &nip47.ListTransactionsParams{} - resp := svc.decodeNip47Request(nip47Request, requestEvent, app, listParams) - if resp != nil { - publishResponse(resp, nostr.Tags{}) - return - } - - resp = svc.checkPermission(nip47Request, requestEvent.NostrId, app, 0) - if resp != nil { - publishResponse(resp, nostr.Tags{}) - return - } - - svc.logger.WithFields(logrus.Fields{ - // TODO: log request fields from listParams - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - }).Info("Fetching transactions") - - limit := listParams.Limit - maxLimit := uint64(50) - if limit == 0 || limit > maxLimit { - // make sure a sensible limit is passed - limit = maxLimit - } - 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{ - // TODO: log request fields from listParams - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - }).Infof("Failed to fetch transactions: %v", err) - - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, - Message: err.Error(), - }, - }, nostr.Tags{}) - return - } - - responsePayload := &nip47.ListTransactionsResponse{ - Transactions: transactions, - } - - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Result: responsePayload, - }, nostr.Tags{}) -} diff --git a/handle_lookup_invoice_request.go b/handle_lookup_invoice_request.go deleted file mode 100644 index 352fa8626..000000000 --- a/handle_lookup_invoice_request.go +++ /dev/null @@ -1,87 +0,0 @@ -package main - -import ( - "context" - "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 *nip47.Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*nip47.Response, nostr.Tags)) { - - lookupInvoiceParams := &nip47.LookupInvoiceParams{} - resp := svc.decodeNip47Request(nip47Request, requestEvent, app, lookupInvoiceParams) - if resp != nil { - publishResponse(resp, nostr.Tags{}) - return - } - - resp = svc.checkPermission(nip47Request, requestEvent.NostrId, app, 0) - if resp != nil { - publishResponse(resp, nostr.Tags{}) - return - } - - svc.logger.WithFields(logrus.Fields{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - "invoice": lookupInvoiceParams.Invoice, - "paymentHash": lookupInvoiceParams.PaymentHash, - }).Info("Looking up invoice") - - paymentHash := lookupInvoiceParams.PaymentHash - - if paymentHash == "" { - paymentRequest, err := decodepay.Decodepay(strings.ToLower(lookupInvoiceParams.Invoice)) - if err != nil { - svc.logger.WithFields(logrus.Fields{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - "invoice": lookupInvoiceParams.Invoice, - }).Errorf("Failed to decode bolt11 invoice: %v", err) - - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, - Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()), - }, - }, nostr.Tags{}) - return - } - paymentHash = paymentRequest.PaymentHash - } - - transaction, err := svc.lnClient.LookupInvoice(ctx, paymentHash) - if err != nil { - 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(&nip47.Response{ - ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, - Message: err.Error(), - }, - }, nostr.Tags{}) - return - } - - responsePayload := &nip47.LookupInvoiceResponse{ - Transaction: *transaction, - } - - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Result: responsePayload, - }, nostr.Tags{}) -} diff --git a/handle_make_invoice_request.go b/handle_make_invoice_request.go deleted file mode 100644 index 8666df1b6..000000000 --- a/handle_make_invoice_request.go +++ /dev/null @@ -1,70 +0,0 @@ -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 *nip47.Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*nip47.Response, nostr.Tags)) { - - makeInvoiceParams := &nip47.MakeInvoiceParams{} - resp := svc.decodeNip47Request(nip47Request, requestEvent, app, makeInvoiceParams) - if resp != nil { - publishResponse(resp, nostr.Tags{}) - return - } - - resp = svc.checkPermission(nip47Request, requestEvent.NostrId, app, 0) - if resp != nil { - publishResponse(resp, nostr.Tags{}) - return - } - - svc.logger.WithFields(logrus.Fields{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - "amount": makeInvoiceParams.Amount, - "description": makeInvoiceParams.Description, - "descriptionHash": makeInvoiceParams.DescriptionHash, - "expiry": makeInvoiceParams.Expiry, - }).Info("Making invoice") - - expiry := makeInvoiceParams.Expiry - if expiry == 0 { - expiry = 86400 - } - - transaction, err := svc.lnClient.MakeInvoice(ctx, makeInvoiceParams.Amount, makeInvoiceParams.Description, makeInvoiceParams.DescriptionHash, expiry) - if err != nil { - svc.logger.WithFields(logrus.Fields{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - "amount": makeInvoiceParams.Amount, - "description": makeInvoiceParams.Description, - "descriptionHash": makeInvoiceParams.DescriptionHash, - "expiry": makeInvoiceParams.Expiry, - }).Infof("Failed to make invoice: %v", err) - - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, - Message: err.Error(), - }, - }, nostr.Tags{}) - return - } - - responsePayload := &nip47.MakeInvoiceResponse{ - Transaction: *transaction, - } - - 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 deleted file mode 100644 index 7fd6e28da..000000000 --- a/handle_multi_pay_invoice_request.go +++ /dev/null @@ -1,139 +0,0 @@ -package main - -import ( - "context" - "fmt" - "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" - decodepay "github.com/nbd-wtf/ln-decodepay" - "github.com/sirupsen/logrus" -) - -// TODO: pass a channel instead of publishResponse function -func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Request *nip47.Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*nip47.Response, nostr.Tags)) { - - multiPayParams := &nip47.MultiPayInvoiceParams{} - resp := svc.decodeNip47Request(nip47Request, requestEvent, app, multiPayParams) - if resp != nil { - publishResponse(resp, nostr.Tags{}) - return - } - - var wg sync.WaitGroup - var mu sync.Mutex - 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 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{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - "bolt11": bolt11, - }).Errorf("Failed to decode bolt11 invoice: %v", err) - - // TODO: Decide what to do if id is empty - dTag := []string{"d", invoiceInfo.Id} - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, - Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()), - }, - }, nostr.Tags{dTag}) - return - } - - invoiceDTagValue := invoiceInfo.Id - if invoiceDTagValue == "" { - invoiceDTagValue = paymentRequest.PaymentHash - } - dTag := []string{"d", invoiceDTagValue} - - resp := svc.checkPermission(nip47Request, requestEvent.NostrId, app, paymentRequest.MSatoshi) - if resp != nil { - publishResponse(resp, nostr.Tags{dTag}) - return - } - - 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{ - "requestEventNostrId": requestEvent.NostrId, - "paymentRequest": bolt11, - "invoiceId": invoiceInfo.Id, - }).Errorf("Failed to process event: %v", insertPaymentResult.Error) - return - } - - svc.logger.WithFields(logrus.Fields{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - "bolt11": bolt11, - }).Info("Sending payment") - - response, err := svc.lnClient.SendPaymentSync(ctx, bolt11) - if err != nil { - 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{ - Event: "nwc_payment_failed", - Properties: map[string]interface{}{ - // "error": fmt.Sprintf("%v", err), - "multi": true, - "invoice": bolt11, - "amount": paymentRequest.MSatoshi / 1000, - }, - }) - - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, - Message: err.Error(), - }, - }, nostr.Tags{dTag}) - return - } - payment.Preimage = &response.Preimage - // TODO: also set fee - - mu.Lock() - svc.db.Save(&payment) - mu.Unlock() - svc.eventPublisher.Publish(&events.Event{ - Event: "nwc_payment_succeeded", - Properties: map[string]interface{}{ - "multi": true, - "amount": paymentRequest.MSatoshi / 1000, - }, - }) - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Result: nip47.PayResponse{ - Preimage: response.Preimage, - FeesPaid: response.Fee, - }, - }, nostr.Tags{dTag}) - }(invoiceInfo) - } - - wg.Wait() -} diff --git a/handle_multi_pay_keysend_request.go b/handle_multi_pay_keysend_request.go deleted file mode 100644 index b1acd07a6..000000000 --- a/handle_multi_pay_keysend_request.go +++ /dev/null @@ -1,109 +0,0 @@ -package main - -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 *nip47.Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*nip47.Response, nostr.Tags)) { - - multiPayParams := &nip47.MultiPayKeysendParams{} - resp := svc.decodeNip47Request(nip47Request, requestEvent, app, multiPayParams) - if resp != nil { - publishResponse(resp, nostr.Tags{}) - return - } - - var wg sync.WaitGroup - var mu sync.Mutex - for _, keysendInfo := range multiPayParams.Keysends { - wg.Add(1) - go func(keysendInfo nip47.MultiPayKeysendElement) { - defer wg.Done() - - keysendDTagValue := keysendInfo.Id - if keysendDTagValue == "" { - keysendDTagValue = keysendInfo.Pubkey - } - dTag := []string{"d", keysendDTagValue} - - resp := svc.checkPermission(nip47Request, requestEvent.NostrId, app, keysendInfo.Amount) - if resp != nil { - publishResponse(resp, nostr.Tags{dTag}) - return - } - - 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{ - "requestEventNostrId": requestEvent.NostrId, - "recipientPubkey": keysendInfo.Pubkey, - "keysendId": keysendInfo.Id, - }).Errorf("Failed to process event: %v", insertPaymentResult.Error) - return - } - - svc.logger.WithFields(logrus.Fields{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - "recipientPubkey": keysendInfo.Pubkey, - }).Info("Sending payment") - - preimage, err := svc.lnClient.SendKeysend(ctx, keysendInfo.Amount, keysendInfo.Pubkey, keysendInfo.Preimage, keysendInfo.TLVRecords) - if err != nil { - 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{ - Event: "nwc_payment_failed", - Properties: map[string]interface{}{ - // "error": fmt.Sprintf("%v", err), - "keysend": true, - "multi": true, - "amount": keysendInfo.Amount / 1000, - }, - }) - - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, - Message: err.Error(), - }, - }, nostr.Tags{dTag}) - return - } - payment.Preimage = &preimage - mu.Lock() - svc.db.Save(&payment) - mu.Unlock() - svc.eventPublisher.Publish(&events.Event{ - Event: "nwc_payment_succeeded", - Properties: map[string]interface{}{ - "keysend": true, - "multi": true, - "amount": keysendInfo.Amount / 1000, - }, - }) - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Result: nip47.PayResponse{ - Preimage: preimage, - }, - }, nostr.Tags{dTag}) - }(keysendInfo) - } - - wg.Wait() -} diff --git a/handle_pay_keysend_request.go b/handle_pay_keysend_request.go deleted file mode 100644 index 97c867a4d..000000000 --- a/handle_pay_keysend_request.go +++ /dev/null @@ -1,86 +0,0 @@ -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 *nip47.Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*nip47.Response, nostr.Tags)) { - - payParams := &nip47.KeysendParams{} - resp := svc.decodeNip47Request(nip47Request, requestEvent, app, payParams) - if resp != nil { - publishResponse(resp, nostr.Tags{}) - return - } - - resp = svc.checkPermission(nip47Request, requestEvent.NostrId, app, payParams.Amount) - if resp != nil { - publishResponse(resp, nostr.Tags{}) - return - } - - payment := db.Payment{App: *app, RequestEvent: *requestEvent, Amount: uint(payParams.Amount / 1000)} - err := svc.db.Create(&payment).Error - if err != nil { - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, - Message: err.Error(), - }, - }, nostr.Tags{}) - return - } - - svc.logger.WithFields(logrus.Fields{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - "senderPubkey": payParams.Pubkey, - }).Info("Sending payment") - - preimage, err := svc.lnClient.SendKeysend(ctx, payParams.Amount, payParams.Pubkey, payParams.Preimage, payParams.TLVRecords) - if err != nil { - 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{ - Event: "nwc_payment_failed", - Properties: map[string]interface{}{ - // "error": fmt.Sprintf("%v", err), - "keysend": true, - "amount": payParams.Amount / 1000, - }, - }) - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, - Message: err.Error(), - }, - }, nostr.Tags{}) - return - } - payment.Preimage = &preimage - svc.db.Save(&payment) - svc.eventPublisher.Publish(&events.Event{ - Event: "nwc_payment_succeeded", - Properties: map[string]interface{}{ - "keysend": true, - "amount": payParams.Amount / 1000, - }, - }) - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Result: nip47.PayResponse{ - Preimage: preimage, - }, - }, nostr.Tags{}) -} diff --git a/handle_payment_request.go b/handle_payment_request.go deleted file mode 100644 index 16afba69f..000000000 --- a/handle_payment_request.go +++ /dev/null @@ -1,114 +0,0 @@ -package main - -import ( - "context" - "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" - decodepay "github.com/nbd-wtf/ln-decodepay" - "github.com/sirupsen/logrus" -) - -func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, nip47Request *nip47.Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*nip47.Response, nostr.Tags)) { - - payParams := &nip47.PayParams{} - resp := svc.decodeNip47Request(nip47Request, requestEvent, app, payParams) - if resp != nil { - publishResponse(resp, nostr.Tags{}) - return - } - - bolt11 := payParams.Invoice - // Convert invoice to lowercase string - bolt11 = strings.ToLower(bolt11) - paymentRequest, err := decodepay.Decodepay(bolt11) - if err != nil { - svc.logger.WithFields(logrus.Fields{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - "bolt11": bolt11, - }).Errorf("Failed to decode bolt11 invoice: %v", err) - - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, - Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()), - }, - }, nostr.Tags{}) - return - } - - resp = svc.checkPermission(nip47Request, requestEvent.NostrId, app, paymentRequest.MSatoshi) - if resp != nil { - publishResponse(resp, nostr.Tags{}) - return - } - - payment := db.Payment{App: *app, RequestEvent: *requestEvent, PaymentRequest: bolt11, Amount: uint(paymentRequest.MSatoshi / 1000)} - err = svc.db.Create(&payment).Error - if err != nil { - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, - Message: err.Error(), - }, - }, nostr.Tags{}) - return - } - - svc.logger.WithFields(logrus.Fields{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - "bolt11": bolt11, - }).Info("Sending payment") - - response, err := svc.lnClient.SendPaymentSync(ctx, bolt11) - if err != nil { - 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{ - Event: "nwc_payment_failed", - Properties: map[string]interface{}{ - // "error": fmt.Sprintf("%v", err), - "invoice": bolt11, - "amount": paymentRequest.MSatoshi / 1000, - }, - }) - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, - Message: err.Error(), - }, - }, nostr.Tags{}) - return - } - payment.Preimage = &response.Preimage - // TODO: save payment fee - svc.db.Save(&payment) - - svc.eventPublisher.Publish(&events.Event{ - Event: "nwc_payment_succeeded", - Properties: map[string]interface{}{ - "bolt11": bolt11, - "amount": paymentRequest.MSatoshi / 1000, - }, - }) - - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Result: nip47.PayResponse{ - Preimage: response.Preimage, - FeesPaid: response.Fee, - }, - }, nostr.Tags{}) -} diff --git a/handle_sign_message_request.go b/handle_sign_message_request.go deleted file mode 100644 index bf4e22eb7..000000000 --- a/handle_sign_message_request.go +++ /dev/null @@ -1,56 +0,0 @@ -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 *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{}) - return - } - - resp = svc.checkPermission(nip47Request, requestEvent.NostrId, app, 0) - if resp != nil { - publishResponse(resp, nostr.Tags{}) - return - } - - 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{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - }).Infof("Failed to sign message: %v", err) - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, - Message: err.Error(), - }, - }, nostr.Tags{}) - return - } - - responsePayload := nip47.SignMessageResponse{ - Message: signParams.Message, - Signature: signature, - } - - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Result: responsePayload, - }, nostr.Tags{}) -} diff --git a/http/http_service.go b/http/http_service.go index 905d3dbeb..5fe946fe5 100644 --- a/http/http_service.go +++ b/http/http_service.go @@ -12,14 +12,13 @@ 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" - "github.com/getAlby/nostr-wallet-connect/lsp" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/getAlby/nostr-wallet-connect/service" "github.com/getAlby/nostr-wallet-connect/api" @@ -30,9 +29,8 @@ type HttpService struct { api api.API albyHttpSvc *alby.AlbyHttpService cfg config.Config - db *gorm.DB - logger *logrus.Logger eventPublisher events.EventPublisher + db *gorm.DB } const ( @@ -40,14 +38,13 @@ const ( sessionCookieAuthKey = "authenticated" ) -func NewHttpService(svc service.Service, logger *logrus.Logger, db *gorm.DB, eventPublisher events.EventPublisher) *HttpService { +func NewHttpService(svc service.Service, eventPublisher events.EventPublisher) *HttpService { return &HttpService{ - api: api.NewAPI(svc, logger, db), - albyHttpSvc: alby.NewAlbyHttpService(svc.GetAlbyOAuthSvc(), logger, svc.GetConfig().GetEnv()), + api: api.NewAPI(svc, svc.GetDB(), svc.GetConfig(), svc.GetKeys(), svc.GetAlbyOAuthSvc(), svc.GetEventPublisher()), + albyHttpSvc: alby.NewAlbyHttpService(svc.GetAlbyOAuthSvc(), svc.GetConfig().GetEnv()), cfg: svc.GetConfig(), - db: db, - logger: logger, eventPublisher: eventPublisher, + db: svc.GetDB(), } } @@ -258,7 +255,7 @@ func (httpSvc *HttpService) saveSessionCookie(c echo.Context) error { sess.Values[sessionCookieAuthKey] = true err := sess.Save(c.Request(), c.Response()) if err != nil { - httpSvc.logger.WithError(err).Error("Failed to save session") + logger.Logger.WithError(err).Error("Failed to save session") } return err } @@ -411,7 +408,7 @@ func (httpSvc *HttpService) mempoolApiHandler(c echo.Context) error { response, err := httpSvc.api.RequestMempoolApi(endpoint) if err != nil { - httpSvc.logger.WithField("endpoint", endpoint).WithError(err).Error("Failed to request mempool API") + logger.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()), }) @@ -504,14 +501,14 @@ func (httpSvc *HttpService) closeChannelHandler(c echo.Context) error { func (httpSvc *HttpService) newInstantChannelInvoiceHandler(c echo.Context) error { ctx := c.Request().Context() - var newWrappedInvoiceRequest lsp.NewInstantChannelInvoiceRequest + var newWrappedInvoiceRequest api.NewInstantChannelInvoiceRequest if err := c.Bind(&newWrappedInvoiceRequest); err != nil { return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: fmt.Sprintf("Bad request: %s", err.Error()), }) } - newWrappedInvoiceResponse, err := httpSvc.api.GetLSPService().NewInstantChannelInvoice(ctx, &newWrappedInvoiceRequest) + newWrappedInvoiceResponse, err := httpSvc.api.NewInstantChannelInvoice(ctx, &newWrappedInvoiceRequest) if err != nil { return c.JSON(http.StatusInternalServerError, ErrorResponse{ @@ -604,8 +601,10 @@ func (httpSvc *HttpService) appsListHandler(c echo.Context) error { } func (httpSvc *HttpService) appsShowHandler(c echo.Context) error { - app := db.App{} - findResult := httpSvc.db.Where("nostr_pubkey = ?", c.Param("pubkey")).First(&app) + + // TODO: move this to DB service + dbApp := db.App{} + findResult := httpSvc.db.Where("nostr_pubkey = ?", c.Param("pubkey")).First(&dbApp) if findResult.RowsAffected == 0 { return c.JSON(http.StatusNotFound, ErrorResponse{ @@ -613,7 +612,7 @@ func (httpSvc *HttpService) appsShowHandler(c echo.Context) error { }) } - response := httpSvc.api.GetApp(&app) + response := httpSvc.api.GetApp(&dbApp) return c.JSON(http.StatusOK, response) } @@ -626,8 +625,9 @@ func (httpSvc *HttpService) appsUpdateHandler(c echo.Context) error { }) } - app := db.App{} - findResult := httpSvc.db.Where("nostr_pubkey = ?", c.Param("pubkey")).First(&app) + // TODO: move this to DB service + dbApp := db.App{} + findResult := httpSvc.db.Where("nostr_pubkey = ?", c.Param("pubkey")).First(&dbApp) if findResult.RowsAffected == 0 { return c.JSON(http.StatusNotFound, ErrorResponse{ @@ -635,10 +635,10 @@ func (httpSvc *HttpService) appsUpdateHandler(c echo.Context) error { }) } - err := httpSvc.api.UpdateApp(&app, &requestData) + err := httpSvc.api.UpdateApp(&dbApp, &requestData) if err != nil { - httpSvc.logger.WithError(err).Error("Failed to update app") + logger.Logger.WithError(err).Error("Failed to update app") return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to update app: %v", err), }) @@ -654,8 +654,9 @@ func (httpSvc *HttpService) appsDeleteHandler(c echo.Context) error { Message: "Invalid pubkey parameter", }) } - app := db.App{} - result := httpSvc.db.Where("nostr_pubkey = ?", pubkey).First(&app) + // TODO: move this to DB service + dbApp := db.App{} + result := httpSvc.db.Where("nostr_pubkey = ?", pubkey).First(&dbApp) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return c.JSON(http.StatusNotFound, ErrorResponse{ @@ -667,7 +668,7 @@ func (httpSvc *HttpService) appsDeleteHandler(c echo.Context) error { }) } - if err := httpSvc.api.DeleteApp(&app); err != nil { + if err := httpSvc.api.DeleteApp(&dbApp); err != nil { return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: "Failed to delete app", }) @@ -686,7 +687,7 @@ func (httpSvc *HttpService) appsCreateHandler(c echo.Context) error { responseBody, err := httpSvc.api.CreateApp(&requestData) if err != nil { - httpSvc.logger.WithField("requestData", requestData).WithError(err).Error("Failed to save app") + logger.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), }) @@ -789,7 +790,7 @@ func (httpSvc *HttpService) createBackupHandler(c echo.Context) error { } var buffer bytes.Buffer - err := httpSvc.api.GetBackupService().CreateBackup(backupRequest.UnlockPassword, &buffer) + err := httpSvc.api.CreateBackup(backupRequest.UnlockPassword, &buffer) if err != nil { return c.String(500, fmt.Sprintf("Failed to create backup: %v", err)) } @@ -827,7 +828,7 @@ func (httpSvc *HttpService) restoreBackupHandler(c echo.Context) error { } defer file.Close() - err = httpSvc.api.GetBackupService().RestoreBackup(password, file) + err = httpSvc.api.RestoreBackup(password, file) if err != nil { return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to restore backup: %v", err), diff --git a/lnclient/breez/breez.go b/lnclient/breez/breez.go index 04615a6db..cbe1401f1 100644 --- a/lnclient/breez/breez.go +++ b/lnclient/breez/breez.go @@ -17,21 +17,19 @@ import ( "github.com/sirupsen/logrus" "github.com/getAlby/nostr-wallet-connect/lnclient" - "github.com/getAlby/nostr-wallet-connect/nip47" + "github.com/getAlby/nostr-wallet-connect/logger" ) type BreezService struct { listener *BreezListener svc *breez_sdk.BlockingBreezServices - logger *logrus.Logger } type BreezListener struct { - logger *logrus.Logger } func (listener BreezListener) Log(l breez_sdk.LogEntry) { if l.Level != "TRACE" && l.Level != "DEBUG" { - listener.logger.WithField("level", l.Level).Print(l.Line) + logger.Logger.WithField("level", l.Level).Print(l.Line) } } @@ -39,7 +37,7 @@ func (BreezListener) OnEvent(e breez_sdk.BreezEvent) { log.Printf("received event %#v", e) } -func NewBreezService(logger *logrus.Logger, mnemonic, apiKey, inviteCode, workDir string) (result lnclient.LNClient, err error) { +func NewBreezService(mnemonic, apiKey, inviteCode, workDir string) (result lnclient.LNClient, err error) { if mnemonic == "" || apiKey == "" || inviteCode == "" || workDir == "" { return nil, errors.New("one or more required breez configuration are missing") } @@ -59,9 +57,7 @@ func NewBreezService(logger *logrus.Logger, mnemonic, apiKey, inviteCode, workDi InviteCode: &inviteCode, }, } - listener := BreezListener{ - logger: logger, - } + listener := BreezListener{} config := breez_sdk.DefaultConfig(breez_sdk.EnvironmentTypeProduction, apiKey, nodeConfig) config.WorkingDir = workDir breez_sdk.SetLogStream(listener) @@ -74,15 +70,15 @@ func NewBreezService(logger *logrus.Logger, mnemonic, apiKey, inviteCode, workDi return nil, err } if err == nil { - logger.WithField("status", healthCheck.Status).Info("Current service status") + logger.Logger.WithField("status", healthCheck.Status).Info("Current service status") } nodeInfo, err := svc.NodeInfo() if err != nil { return nil, err } - logger.WithField("info", nodeInfo).Info("Node info") - logger.WithFields(logrus.Fields{ + logger.Logger.WithField("info", nodeInfo).Info("Node info") + logger.Logger.WithFields(logrus.Fields{ "ln balance": nodeInfo.ChannelsBalanceMsat, "balance": nodeInfo.OnchainBalanceMsat, "max_payable_msat": nodeInfo.MaxPayableMsat, @@ -95,7 +91,6 @@ func NewBreezService(logger *logrus.Logger, mnemonic, apiKey, inviteCode, workDi return &BreezService{ listener: &listener, svc: svc, - logger: logger, }, nil } @@ -121,7 +116,7 @@ func (bs *BreezService) SendPaymentSync(ctx context.Context, payReq string) (*ln } -func (bs *BreezService) SendKeysend(ctx context.Context, amount int64, destination, preimage string, custom_records []lnclient.TLVRecord) (preImage string, err error) { +func (bs *BreezService) SendKeysend(ctx context.Context, amount uint64, destination, preimage string, custom_records []lnclient.TLVRecord) (preImage string, err error) { extraTlvs := []breez_sdk.TlvEntry{} for _, record := range custom_records { extraTlvs = append(extraTlvs, breez_sdk.TlvEntry{ @@ -132,7 +127,7 @@ func (bs *BreezService) SendKeysend(ctx context.Context, amount int64, destinati sendSpontaneousPaymentRequest := breez_sdk.SendSpontaneousPaymentRequest{ NodeId: destination, - AmountMsat: uint64(amount), + AmountMsat: amount, ExtraTlvs: &extraTlvs, } resp, err := bs.svc.SendSpontaneousPayment(sendSpontaneousPaymentRequest) @@ -154,7 +149,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 *nip47.Transaction, err error) { +func (bs *BreezService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *lnclient.Transaction, err error) { expiry32 := uint32(expiry) receivePaymentRequest := breez_sdk.ReceivePaymentRequest{ // amount provided in msat @@ -167,7 +162,7 @@ func (bs *BreezService) MakeInvoice(ctx context.Context, amount int64, descripti return nil, err } - tx := &nip47.Transaction{ + tx := &lnclient.Transaction{ Type: "incoming", Invoice: resp.LnInvoice.Bolt11, Preimage: hex.EncodeToString(resp.LnInvoice.PaymentSecret), @@ -192,7 +187,7 @@ func (bs *BreezService) MakeInvoice(ctx context.Context, amount int64, descripti return tx, nil } -func (bs *BreezService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *nip47.Transaction, err error) { +func (bs *BreezService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *lnclient.Transaction, err error) { log.Printf("p: %v", paymentHash) payment, err := bs.svc.PaymentByHash(paymentHash) if err != nil { @@ -210,7 +205,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 []nip47.Transaction, err error) { +func (bs *BreezService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []lnclient.Transaction, err error) { request := breez_sdk.ListPaymentsRequest{} if limit > 0 { @@ -235,7 +230,7 @@ func (bs *BreezService) ListTransactions(ctx context.Context, from, until, limit return nil, err } - transactions = []nip47.Transaction{} + transactions = []lnclient.Transaction{} for _, payment := range payments { if payment.PaymentType != breez_sdk.PaymentTypeReceived && payment.PaymentType != breez_sdk.PaymentTypeSent { // skip other types of payments for now @@ -281,7 +276,7 @@ func (bs *BreezService) CloseChannel(ctx context.Context, closeChannelRequest *l return nil, nil } -func breezPaymentToTransaction(payment *breez_sdk.Payment) (*nip47.Transaction, error) { +func breezPaymentToTransaction(payment *breez_sdk.Payment) (*lnclient.Transaction, error) { var lnDetails breez_sdk.PaymentDetailsLn if payment.Details != nil { lnDetails, _ = payment.Details.(breez_sdk.PaymentDetailsLn) @@ -313,7 +308,7 @@ func breezPaymentToTransaction(payment *breez_sdk.Payment) (*nip47.Transaction, descriptionHash = paymentRequest.DescriptionHash } - tx := &nip47.Transaction{ + tx := &lnclient.Transaction{ Type: txType, Invoice: lnDetails.Data.Bolt11, Preimage: lnDetails.Data.PaymentPreimage, @@ -344,10 +339,10 @@ func (bs *BreezService) GetNewOnchainAddress(ctx context.Context) (string, error /*swapInfo, err := bs.svc.ReceiveOnchain(breez_sdk.ReceiveOnchainRequest{}) if err != nil { - bs.logger.Errorf("Failed to get onchain address: %v", err) + logger.Logger.Errorf("Failed to get onchain address: %v", err) return "", err } - bs.logger.Infof("This address has deposit limits! Min: %d Max: %d", swapInfo.MinAllowedDeposit, swapInfo.MaxAllowedDeposit) + logger.Logger.Infof("This address has deposit limits! Min: %d Max: %d", swapInfo.MinAllowedDeposit, swapInfo.MaxAllowedDeposit) return swapInfo.BitcoinAddress, nil*/ return "", nil @@ -357,7 +352,7 @@ func (bs *BreezService) GetOnchainBalance(ctx context.Context) (*lnclient.Onchai response, err := bs.svc.NodeInfo() if err != nil { - bs.logger.Errorf("Failed to get node info: %v", err) + logger.Logger.Errorf("Failed to get node info: %v", err) return nil, err } @@ -374,7 +369,7 @@ func (bs *BreezService) RedeemOnchainFunds(ctx context.Context, toAddress string recommendedFees, err := bs.svc.RecommendedFees() if err != nil { - bs.logger.Errorf("Failed to get recommended fees info: %v", err) + logger.Logger.Errorf("Failed to get recommended fees info: %v", err) return "", err } @@ -383,20 +378,20 @@ func (bs *BreezService) RedeemOnchainFunds(ctx context.Context, toAddress string prepareRedeemOnchainFundsResponse, err := bs.svc.PrepareRedeemOnchainFunds(prepareReq) if err != nil { - bs.logger.Errorf("Failed to prepare onchain address: %v", err) + logger.Logger.Errorf("Failed to prepare onchain address: %v", err) return "", err } - bs.logger.Infof("PrepareRedeemOnchainFunds response: %#v", prepareRedeemOnchainFundsResponse) + logger.Logger.Infof("PrepareRedeemOnchainFunds response: %#v", prepareRedeemOnchainFundsResponse) redeemReq := breez_sdk.RedeemOnchainFundsRequest{SatPerVbyte: satPerVbyte, ToAddress: toAddress} redeemOnchainFundsResponse, err := bs.svc.RedeemOnchainFunds(redeemReq) if err != nil { - bs.logger.Errorf("Failed to redeem onchain funds: %v", err) + logger.Logger.Errorf("Failed to redeem onchain funds: %v", err) return "", err } - bs.logger.Infof("RedeemOnchainFunds response: %#v", redeemOnchainFundsResponse) + logger.Logger.Infof("RedeemOnchainFunds response: %#v", redeemOnchainFundsResponse) return hex.EncodeToString(redeemOnchainFundsResponse.Txid), nil } diff --git a/lnclient/breez/breez_stub.go b/lnclient/breez/breez_stub.go index de443cb93..9692e0eff 100644 --- a/lnclient/breez/breez_stub.go +++ b/lnclient/breez/breez_stub.go @@ -3,12 +3,10 @@ package breez import ( - "github.com/sirupsen/logrus" - "github.com/getAlby/nostr-wallet-connect/lnclient" ) -func NewBreezService(logger *logrus.Logger, mnemonic, apiKey, inviteCode, workDir string) (result lnclient.LNClient, err error) { +func NewBreezService(mnemonic, apiKey, inviteCode, workDir string) (result lnclient.LNClient, err error) { panic("not implemented") return nil, nil } diff --git a/lnclient/cashu/cashu.go b/lnclient/cashu/cashu.go index eaec306ab..d2905a014 100644 --- a/lnclient/cashu/cashu.go +++ b/lnclient/cashu/cashu.go @@ -12,17 +12,16 @@ import ( "github.com/elnosh/gonuts/wallet" "github.com/elnosh/gonuts/wallet/storage" "github.com/getAlby/nostr-wallet-connect/lnclient" - "github.com/getAlby/nostr-wallet-connect/nip47" + "github.com/getAlby/nostr-wallet-connect/logger" decodepay "github.com/nbd-wtf/ln-decodepay" "github.com/sirupsen/logrus" ) type CashuService struct { - logger *logrus.Logger wallet *wallet.Wallet } -func NewCashuService(logger *logrus.Logger, workDir string, mintUrl string) (result lnclient.LNClient, err error) { +func NewCashuService(workDir string, mintUrl string) (result lnclient.LNClient, err error) { if workDir == "" { return nil, errors.New("one or more required cashu configuration are missing") } @@ -38,17 +37,16 @@ func NewCashuService(logger *logrus.Logger, workDir string, mintUrl string) (res return nil, err } - logger.WithField("mintUrl", mintUrl).Info("Setting up cashu wallet") + logger.Logger.WithField("mintUrl", mintUrl).Info("Setting up cashu wallet") config := wallet.Config{WalletPath: newpath, CurrentMintURL: mintUrl} wallet, err := wallet.LoadWallet(config) if err != nil { - logger.WithError(err).Error("Failed to load cashu wallet") + logger.Logger.WithError(err).Error("Failed to load cashu wallet") return nil, err } cs := CashuService{ - logger: logger, wallet: wallet, } @@ -62,7 +60,7 @@ func (cs *CashuService) Shutdown() error { func (cs *CashuService) SendPaymentSync(ctx context.Context, invoice string) (response *lnclient.PayInvoiceResponse, err error) { meltResponse, err := cs.wallet.Melt(invoice, cs.wallet.CurrentMint()) if err != nil { - cs.logger.WithError(err).Error("Failed to melt invoice") + logger.Logger.WithError(err).Error("Failed to melt invoice") return nil, err } @@ -73,13 +71,13 @@ func (cs *CashuService) SendPaymentSync(ctx context.Context, invoice string) (re // TODO: get fee from melt response cashuInvoice, err := cs.wallet.GetInvoiceByPaymentRequest(invoice) if err != nil { - cs.logger.WithField("invoice", invoice).WithError(err).Error("Failed to get invoice after melting") + logger.Logger.WithField("invoice", invoice).WithError(err).Error("Failed to get invoice after melting") return nil, err } transaction, err := cs.cashuInvoiceToTransaction(cashuInvoice) if err != nil { - cs.logger.WithField("invoice", invoice).WithError(err).Error("Failed to convert invoice to transaction") + logger.Logger.WithField("invoice", invoice).WithError(err).Error("Failed to convert invoice to transaction") return nil, err } @@ -91,7 +89,7 @@ func (cs *CashuService) SendPaymentSync(ctx context.Context, invoice string) (re }, nil } -func (cs *CashuService) SendKeysend(ctx context.Context, amount int64, destination, preimage string, custom_records []lnclient.TLVRecord) (preImage string, err error) { +func (cs *CashuService) SendKeysend(ctx context.Context, amount uint64, destination, preimage string, custom_records []lnclient.TLVRecord) (preImage string, err error) { return "", errors.New("Keysend not supported") } @@ -106,16 +104,16 @@ func (cs *CashuService) GetBalance(ctx context.Context) (balance int64, err erro return int64(totalBalance * 1000), nil } -func (cs *CashuService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *nip47.Transaction, err error) { +func (cs *CashuService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *lnclient.Transaction, err error) { mintResponse, err := cs.wallet.RequestMint(uint64(amount / 1000)) if err != nil { - cs.logger.WithError(err).Error("Failed to mint") + logger.Logger.WithError(err).Error("Failed to mint") return nil, err } paymentRequest, err := decodepay.Decodepay(mintResponse.Request) if err != nil { - cs.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "invoice": mintResponse.Request, }).WithError(err).Error("Failed to decode bolt11 invoice") return nil, err @@ -124,11 +122,11 @@ func (cs *CashuService) MakeInvoice(ctx context.Context, amount int64, descripti return cs.LookupInvoice(ctx, paymentRequest.PaymentHash) } -func (cs *CashuService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *nip47.Transaction, err error) { +func (cs *CashuService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *lnclient.Transaction, err error) { cashuInvoice := cs.wallet.GetInvoiceByPaymentHash(paymentHash) if cashuInvoice == nil { - cs.logger.WithField("paymentHash", paymentHash).Error("Failed to lookup payment request by payment hash") + logger.Logger.WithField("paymentHash", paymentHash).Error("Failed to lookup payment request by payment hash") return nil, errors.New("failed to lookup payment request by payment hash") } @@ -139,8 +137,8 @@ func (cs *CashuService) LookupInvoice(ctx context.Context, paymentHash string) ( return transaction, nil } -func (cs *CashuService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []nip47.Transaction, err error) { - transactions = []nip47.Transaction{} +func (cs *CashuService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []lnclient.Transaction, err error) { + transactions = []lnclient.Transaction{} invoices := cs.wallet.GetAllInvoices() @@ -256,7 +254,7 @@ func (cs *CashuService) SendSpontaneousPaymentProbes(ctx context.Context, amount func (cs *CashuService) GetBalances(ctx context.Context) (*lnclient.BalancesResponse, error) { balance, err := cs.GetBalance(ctx) if err != nil { - cs.logger.WithError(err).Error("Failed to get balance") + logger.Logger.WithError(err).Error("Failed to get balance") return nil, err } return &lnclient.BalancesResponse{ @@ -275,10 +273,10 @@ func (cs *CashuService) GetBalances(ctx context.Context) (*lnclient.BalancesResp }, nil } -func (cs *CashuService) cashuInvoiceToTransaction(cashuInvoice *storage.Invoice) (*nip47.Transaction, error) { +func (cs *CashuService) cashuInvoiceToTransaction(cashuInvoice *storage.Invoice) (*lnclient.Transaction, error) { paymentRequest, err := decodepay.Decodepay(cashuInvoice.PaymentRequest) if err != nil { - cs.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "invoice": cashuInvoice.PaymentRequest, }).WithError(err).Error("Failed to decode bolt11 invoice") return nil, err @@ -301,7 +299,7 @@ func (cs *CashuService) cashuInvoiceToTransaction(cashuInvoice *storage.Invoice) invoiceType = "incoming" } - return &nip47.Transaction{ + return &lnclient.Transaction{ Type: invoiceType, Invoice: cashuInvoice.PaymentRequest, PaymentHash: paymentRequest.PaymentHash, @@ -318,19 +316,19 @@ func (cs *CashuService) cashuInvoiceToTransaction(cashuInvoice *storage.Invoice) func (cs *CashuService) checkInvoice(cashuInvoice *storage.Invoice) { if cashuInvoice.TransactionType == storage.Mint && !cashuInvoice.Paid { - cs.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "paymentHash": cashuInvoice.PaymentHash, }).Info("Checking unpaid invoice") proofs, err := cs.wallet.MintTokens(cashuInvoice.Id) if err != nil { - cs.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "paymentHash": cashuInvoice.PaymentHash, }).WithError(err).Warn("failed to mint") } if proofs != nil { - cs.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "paymentHash": cashuInvoice.PaymentHash, "amount": proofs.Amount(), }).Info("sats successfully minted") diff --git a/lnclient/greenlight/greenlight.go b/lnclient/greenlight/greenlight.go index 03cee213e..b21f30daa 100644 --- a/lnclient/greenlight/greenlight.go +++ b/lnclient/greenlight/greenlight.go @@ -21,18 +21,17 @@ import ( "github.com/getAlby/nostr-wallet-connect/config" "github.com/getAlby/nostr-wallet-connect/lnclient" - "github.com/getAlby/nostr-wallet-connect/nip47" + "github.com/getAlby/nostr-wallet-connect/logger" ) type GreenlightService struct { workdir string client *glalby.BlockingGreenlightAlbyClient - logger *logrus.Logger } const DEVICE_CREDENTIALS_KEY = "GreenlightCreds" -func NewGreenlightService(cfg config.Config, logger *logrus.Logger, mnemonic, inviteCode, workDir, encryptionKey string) (result lnclient.LNClient, err error) { +func NewGreenlightService(cfg config.Config, 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") } @@ -52,22 +51,22 @@ func NewGreenlightService(cfg config.Config, logger *logrus.Logger, mnemonic, in credentials = &glalby.GreenlightCredentials{ GlCreds: existingDeviceCreds, } - logger.Info("Using saved greenlight credentials") + logger.Logger.Info("Using saved greenlight credentials") } if credentials == nil { - logger.Info("No greenlight credentials found, attempting to recover existing node") + logger.Logger.Info("No greenlight credentials found, attempting to recover existing node") recoveredCredentials, err := glalby.Recover(mnemonic) credentials = &recoveredCredentials if err != nil { - logger.Errorf("Failed to recover node: %v", err) - logger.Infof("Trying to register instead...") + logger.Logger.Errorf("Failed to recover node: %v", err) + logger.Logger.Infof("Trying to register instead...") recoveredCredentials, err := glalby.Register(mnemonic, inviteCode) credentials = &recoveredCredentials if err != nil { - logger.Fatalf("Failed to register new node") + logger.Logger.Fatalf("Failed to register new node") } } @@ -90,7 +89,6 @@ func NewGreenlightService(cfg config.Config, logger *logrus.Logger, mnemonic, in gs := GreenlightService{ workdir: newpath, client: client, - logger: logger, } nodeInfo, err := client.GetInfo() @@ -107,7 +105,7 @@ func NewGreenlightService(cfg config.Config, logger *logrus.Logger, mnemonic, in func (gs *GreenlightService) Shutdown() error { _, err := gs.client.Shutdown() if err != nil { - gs.logger.WithError(err).Error("Failed to shutdown greenlight node") + logger.Logger.WithError(err).Error("Failed to shutdown greenlight node") return err } return nil @@ -119,7 +117,7 @@ func (gs *GreenlightService) SendPaymentSync(ctx context.Context, payReq string) }) if err != nil { - gs.logger.Errorf("Failed to send payment: %v", err) + logger.Logger.Errorf("Failed to send payment: %v", err) return nil, err } log.Printf("SendPaymentSync succeeded: %v", response.Preimage) @@ -128,7 +126,7 @@ func (gs *GreenlightService) SendPaymentSync(ctx context.Context, payReq string) }, nil } -func (gs *GreenlightService) SendKeysend(ctx context.Context, amount int64, destination, preimage string, custom_records []lnclient.TLVRecord) (preImage string, err error) { +func (gs *GreenlightService) SendKeysend(ctx context.Context, amount uint64, destination, preimage string, custom_records []lnclient.TLVRecord) (preImage string, err error) { extraTlvs := []glalby.TlvEntry{} @@ -139,16 +137,15 @@ func (gs *GreenlightService) SendKeysend(ctx context.Context, amount int64, dest }) } - amount_u64 := uint64(amount) // TODO: support passing custom preimage response, err := gs.client.KeySend(glalby.KeySendRequest{ Destination: destination, - AmountMsat: &amount_u64, + AmountMsat: &amount, ExtraTlvs: &extraTlvs, }) if err != nil { - gs.logger.Errorf("Failed to send keysend payment: %v", err) + logger.Logger.Errorf("Failed to send keysend payment: %v", err) return "", err } @@ -159,7 +156,7 @@ func (gs *GreenlightService) GetBalance(ctx context.Context) (balance int64, err response, err := gs.client.ListFunds(glalby.ListFundsRequest{}) if err != nil { - gs.logger.Errorf("Failed to list funds: %v", err) + logger.Logger.Errorf("Failed to list funds: %v", err) return 0, err } @@ -173,7 +170,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 *nip47.Transaction, err error) { +func (gs *GreenlightService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *lnclient.Transaction, err error) { uexpiry := uint64(expiry) // TODO: it seems description hash cannot be passed to greenlight invoice, err := gs.client.MakeInvoice(glalby.MakeInvoiceRequest{ @@ -184,13 +181,13 @@ func (gs *GreenlightService) MakeInvoice(ctx context.Context, amount int64, desc }) if err != nil { - gs.logger.Errorf("MakeInvoice failed: %v", err) + logger.Logger.Errorf("MakeInvoice failed: %v", err) return nil, err } paymentRequest, err := decodepay.Decodepay(strings.ToLower(invoice.Bolt11)) if err != nil { - gs.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "invoice": invoice.Bolt11, }).Errorf("Failed to decode bolt11 invoice: %v", invoice.Bolt11) return nil, err @@ -199,7 +196,7 @@ func (gs *GreenlightService) MakeInvoice(ctx context.Context, amount int64, desc description = paymentRequest.Description descriptionHash = paymentRequest.DescriptionHash expiresAt := int64(invoice.ExpiresAt) - transaction = &nip47.Transaction{ + transaction = &lnclient.Transaction{ Type: "incoming", Invoice: invoice.Bolt11, Description: description, @@ -213,13 +210,13 @@ func (gs *GreenlightService) MakeInvoice(ctx context.Context, amount int64, desc return transaction, nil } -func (gs *GreenlightService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *nip47.Transaction, err error) { +func (gs *GreenlightService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *lnclient.Transaction, err error) { response, err := gs.client.ListInvoices(glalby.ListInvoicesRequest{ PaymentHash: &paymentHash, }) if err != nil { - gs.logger.Errorf("ListInvoices failed: %v", err) + logger.Logger.Errorf("ListInvoices failed: %v", err) return nil, err } @@ -235,22 +232,22 @@ func (gs *GreenlightService) LookupInvoice(ctx context.Context, paymentHash stri transaction, err = gs.greenlightInvoiceToTransaction(&invoice) if err != nil { - gs.logger.Errorf("Failed to map invoice: %v", err) + logger.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 []nip47.Transaction, err error) { +func (gs *GreenlightService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []lnclient.Transaction, err error) { listInvoicesResponse, err := gs.client.ListInvoices(glalby.ListInvoicesRequest{}) if err != nil { - gs.logger.Errorf("ListInvoices failed: %v", err) + logger.Logger.Errorf("ListInvoices failed: %v", err) return nil, err } - transactions = []nip47.Transaction{} + transactions = []lnclient.Transaction{} if err != nil { log.Printf("ListInvoices failed: %v", err) @@ -274,7 +271,7 @@ func (gs *GreenlightService) ListTransactions(ctx context.Context, from, until, listPaymentsResponse, err := gs.client.ListPayments(glalby.ListPaymentsRequest{}) if err != nil { - gs.logger.Errorf("ListPayments failed: %v", err) + logger.Logger.Errorf("ListPayments failed: %v", err) return nil, err } @@ -343,7 +340,7 @@ func (gs *GreenlightService) GetInfo(ctx context.Context) (info *lnclient.NodeIn nodeInfo, err := gs.client.GetInfo() if err != nil { - gs.logger.Errorf("GetInfo failed: %v", err) + logger.Logger.Errorf("GetInfo failed: %v", err) return nil, err } @@ -361,7 +358,7 @@ func (gs *GreenlightService) ListChannels(ctx context.Context) ([]lnclient.Chann response, err := gs.client.ListFunds(glalby.ListFundsRequest{}) if err != nil { - gs.logger.Errorf("Failed to list funds: %v", err) + logger.Logger.Errorf("Failed to list funds: %v", err) return nil, err } @@ -398,7 +395,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.logger.Errorf("GetInfo failed: %v", err) + logger.Logger.Errorf("GetInfo failed: %v", err) return nil, err } return &lnclient.NodeConnectionInfo{ @@ -421,7 +418,7 @@ func (gs *GreenlightService) ConnectPeer(ctx context.Context, connectPeerRequest Port: port, }) if err != nil { - gs.logger.Errorf("ConnectPeer failed: %v", err) + logger.Logger.Errorf("ConnectPeer failed: %v", err) return err } return nil @@ -438,7 +435,7 @@ func (gs *GreenlightService) OpenChannel(ctx context.Context, openChannelRequest // Minconf: &minConf, }) if err != nil { - gs.logger.Errorf("OpenChannel failed: %v", err) + logger.Logger.Errorf("OpenChannel failed: %v", err) return nil, err } @@ -452,7 +449,7 @@ func (gs *GreenlightService) CloseChannel(ctx context.Context, closeChannelReque Id: closeChannelRequest.ChannelId, }) if err != nil { - gs.logger.WithError(err).Error("CloseChannel failed") + logger.Logger.WithError(err).Error("CloseChannel failed") return nil, err } @@ -463,7 +460,7 @@ func (gs *GreenlightService) GetNewOnchainAddress(ctx context.Context) (string, newAddressResponse, err := gs.client.NewAddress(glalby.NewAddressRequest{}) if err != nil { - gs.logger.Errorf("NewAddress failed: %v", err) + logger.Logger.Errorf("NewAddress failed: %v", err) return "", err } if newAddressResponse.Bech32 == nil { @@ -475,10 +472,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.logger.WithField("response", response).Info("Listed funds") + logger.Logger.WithField("response", response).Info("Listed funds") if err != nil { - gs.logger.Errorf("Failed to list funds: %v", err) + logger.Logger.Errorf("Failed to list funds: %v", err) return nil, err } @@ -507,10 +504,10 @@ func (gs *GreenlightService) RedeemOnchainFunds(ctx context.Context, toAddress s Amount: &amountAll, }) if err != nil { - gs.logger.WithError(err).Error("Withdraw failed") + logger.Logger.WithError(err).Error("Withdraw failed") return "", err } - gs.logger.WithField("txId", txId).Info("Redeeming On-Chain funds") + logger.Logger.WithField("txId", txId).Info("Redeeming On-Chain funds") return txId.Txid, nil } @@ -537,14 +534,14 @@ func (gs *GreenlightService) SignMessage(ctx context.Context, message string) (s }) if err != nil { - gs.logger.Errorf("SignMessage failed: %v", err) + logger.Logger.Errorf("SignMessage failed: %v", err) return "", err } return response.Zbase, nil } -func (gs *GreenlightService) greenlightInvoiceToTransaction(invoice *glalby.ListInvoicesInvoice) (*nip47.Transaction, error) { +func (gs *GreenlightService) greenlightInvoiceToTransaction(invoice *glalby.ListInvoicesInvoice) (*lnclient.Transaction, error) { description := "" descriptionHash := "" if invoice.Description != nil { @@ -553,7 +550,7 @@ func (gs *GreenlightService) greenlightInvoiceToTransaction(invoice *glalby.List bolt11 := *invoice.Bolt11 paymentRequest, err := decodepay.Decodepay(strings.ToLower(bolt11)) if err != nil { - gs.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "invoice": bolt11, }).Errorf("Failed to decode bolt11 invoice: %v", bolt11) return nil, err @@ -582,7 +579,7 @@ func (gs *GreenlightService) greenlightInvoiceToTransaction(invoice *glalby.List settledAt = &paidAt } - transaction := &nip47.Transaction{ + transaction := &lnclient.Transaction{ Type: "incoming", Invoice: bolt11, Description: description, @@ -605,14 +602,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.logger.WithError(err).Error("Failed to retrieve onchain balance") + logger.Logger.WithError(err).Error("Failed to retrieve onchain balance") return nil, err } response, err := gs.client.ListFunds(glalby.ListFundsRequest{}) if err != nil { - gs.logger.Errorf("Failed to list funds: %v", err) + logger.Logger.Errorf("Failed to list funds: %v", err) return nil, err } diff --git a/lnclient/ldk/ldk.go b/lnclient/ldk/ldk.go index 2e8f37654..ecdc1b46c 100644 --- a/lnclient/ldk/ldk.go +++ b/lnclient/ldk/ldk.go @@ -25,8 +25,8 @@ import ( "github.com/getAlby/nostr-wallet-connect/config" "github.com/getAlby/nostr-wallet-connect/events" "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/getAlby/nostr-wallet-connect/lsp" - "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/getAlby/nostr-wallet-connect/utils" ) @@ -39,14 +39,13 @@ type LDKService struct { 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, logger *logrus.Logger, cfg config.Config, eventPublisher events.EventPublisher, mnemonic, workDir string, network string, esploraServer string, gossipSource string) (result lnclient.LNClient, err error) { +func NewLDKService(ctx context.Context, 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") } @@ -101,10 +100,10 @@ func NewLDKService(ctx context.Context, logger *logrus.Logger, cfg config.Config builder.SetNetwork(network) builder.SetEsploraServer(esploraServer) if gossipSource != "" { - logger.WithField("gossipSource", gossipSource).Warn("LDK RGS instance set") + logger.Logger.WithField("gossipSource", gossipSource).Warn("LDK RGS instance set") builder.SetGossipSourceRgs(gossipSource) } else { - logger.Warn("No LDK RGS instance set") + logger.Logger.Warn("No LDK RGS instance set") } builder.SetStorageDirPath(filepath.Join(newpath, "./storage")) @@ -116,13 +115,13 @@ func NewLDKService(ctx context.Context, logger *logrus.Logger, cfg config.Config node, err := builder.Build() if err != nil { - logger.Errorf("Failed to create LDK node: %v", err) + logger.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(logger, ldkCtx, ldkEventConsumer) + ldkEventBroadcaster := NewLDKEventBroadcaster(ldkCtx, ldkEventConsumer) ls := LDKService{ workdir: newpath, @@ -131,19 +130,18 @@ func NewLDKService(ctx context.Context, logger *logrus.Logger, cfg config.Config ldkEventBroadcaster: ldkEventBroadcaster, network: network, eventPublisher: eventPublisher, - logger: logger, cfg: cfg, } // TODO: remove when LDK supports this - deleteOldLDKLogs(logger, logDirPath) + deleteOldLDKLogs(logDirPath) go func() { // delete old LDK logs every 24 hours ticker := time.NewTicker(24 * time.Hour) for { select { case <-ticker.C: - deleteOldLDKLogs(logger, logDirPath) + deleteOldLDKLogs(logDirPath) case <-ldkCtx.Done(): return } @@ -176,12 +174,12 @@ func NewLDKService(ctx context.Context, logger *logrus.Logger, cfg config.Config err = node.Start() if err != nil { - logger.Errorf("Failed to start LDK node: %v", err) + logger.Logger.Errorf("Failed to start LDK node: %v", err) return nil, err } nodeId := node.NodeId() - logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "nodeId": nodeId, "status": node.Status(), }).Info("Started LDK node. Syncing wallet...") @@ -189,17 +187,17 @@ func NewLDKService(ctx context.Context, logger *logrus.Logger, cfg config.Config syncStartTime := time.Now() err = node.SyncWallets() if err != nil { - logger.WithError(err).Error("Failed to sync LDK wallets") + logger.Logger.WithError(err).Error("Failed to sync LDK wallets") shutdownErr := ls.Shutdown() if shutdownErr != nil { - logger.WithError(shutdownErr).Error("Failed to shutdown LDK node") + logger.Logger.WithError(shutdownErr).Error("Failed to shutdown LDK node") } return nil, err } ls.lastSync = time.Now() - logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "nodeId": nodeId, "status": node.Status(), "duration": math.Ceil(time.Since(syncStartTime).Seconds()), @@ -214,12 +212,12 @@ func NewLDKService(ctx context.Context, logger *logrus.Logger, cfg config.Config "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226@170.75.163.209:9735", // WoS "02fcc5bfc48e83f06c04483a2985e1c390cb0f35058baa875ad2053858b8e80dbd@35.239.148.251:9735", // Blink } - logger.Info("Connecting to some peers to retrieve P2P gossip data") + logger.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 { - logger.WithError(err).Error("Failed to parse port number") + logger.Logger.WithError(err).Error("Failed to parse port number") continue } err = ls.ConnectPeer(ctx, &lnclient.ConnectPeerRequest{ @@ -228,7 +226,7 @@ func NewLDKService(ctx context.Context, logger *logrus.Logger, cfg config.Config Port: uint16(port), }) if err != nil { - logger.WithField("peer", peer).WithError(err).Error("Failed to connect to peer") + logger.Logger.WithField("peer", peer).WithError(err).Error("Failed to connect to peer") } } } @@ -244,25 +242,25 @@ func NewLDKService(ctx context.Context, logger *logrus.Logger, cfg config.Config case <-time.After(MIN_SYNC_INTERVAL): if time.Since(ls.lastWalletSyncRequest) > MIN_SYNC_INTERVAL && time.Since(ls.lastSync) < MAX_SYNC_INTERVAL { - // ls.logger.Debug("skipping background wallet sync") + // logger.Debug("skipping background wallet sync") continue } - ls.logger.Info("Starting background wallet sync") + logger.Logger.Info("Starting background wallet sync") syncStartTime := time.Now() ls.syncing = true err = node.SyncWallets() ls.syncing = false if err != nil { - logger.WithError(err).Error("Failed to sync LDK wallets") + logger.Logger.WithError(err).Error("Failed to sync LDK wallets") // try again at next MIN_SYNC_INTERVAL continue } ls.lastSync = time.Now() - logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "nodeId": nodeId, "status": node.Status(), "duration": math.Ceil(time.Since(syncStartTime).Seconds()), @@ -276,23 +274,23 @@ func NewLDKService(ctx context.Context, logger *logrus.Logger, cfg config.Config func (ls *LDKService) Shutdown() error { if ls.node == nil { - ls.logger.Info("LDK client already shut down") + logger.Logger.Info("LDK client already shut down") return nil } // make sure nothing else can use it node := ls.node ls.node = nil - ls.logger.Info("shutting down LDK client") - ls.logger.Info("cancelling LDK context") + logger.Logger.Info("shutting down LDK client") + logger.Logger.Info("cancelling LDK context") ls.cancel() for ls.syncing { - ls.logger.Info("Waiting for background sync to finish before stopping LDK node...") + logger.Logger.Info("Waiting for background sync to finish before stopping LDK node...") time.Sleep(1 * time.Second) } - ls.logger.Info("stopping LDK node") + logger.Logger.Info("stopping LDK node") shutdownChannel := make(chan error) go func() { shutdownChannel <- node.Stop() @@ -301,21 +299,21 @@ func (ls *LDKService) Shutdown() error { select { case err := <-shutdownChannel: if err != nil { - ls.logger.WithError(err).Error("Failed to stop LDK node") + logger.Logger.WithError(err).Error("Failed to stop LDK node") // do not return error - we still need to destroy the node } else { - ls.logger.Info("LDK stop node succeeded") + logger.Logger.Info("LDK stop node succeeded") } case <-time.After(120 * time.Second): - ls.logger.Error("Timeout shutting down LDK node after 120 seconds") + logger.Logger.Error("Timeout shutting down LDK node after 120 seconds") } - ls.logger.Info("Destroying node object") + logger.Logger.Info("Destroying node object") node.Destroy() ls.resetRouterInternal() - ls.logger.Info("LDK shutdown complete") + logger.Logger.Info("LDK shutdown complete") return nil } @@ -324,21 +322,21 @@ func (ls *LDKService) resetRouterInternal() { key, err := ls.cfg.Get(resetRouterKey, "") if err != nil { - ls.logger.Error("Failed to retrieve ResetRouter key") + logger.Logger.Error("Failed to retrieve ResetRouter key") } if key != "" { ls.cfg.SetUpdate(resetRouterKey, "", "") - ls.logger.WithField("key", key).Info("Resetting router") + logger.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.logger.Error("Could not find LDK database") + logger.Logger.Error("Could not find LDK database") return } ldkDb, err := sql.Open("sqlite", ldkDbPath) if err != nil { - ls.logger.Error("Could not open LDK DB file") + logger.Logger.Error("Could not open LDK DB file") return } @@ -354,26 +352,26 @@ func (ls *LDKService) resetRouterInternal() { case "NetworkGraph": command = "delete from ldk_node_data where key = 'network_graph';VACUUM;" default: - ls.logger.WithField("key", key).Error("Unknown reset router key") + logger.Logger.WithField("key", key).Error("Unknown reset router key") return } result, err := ldkDb.Exec(command) if err != nil { - ls.logger.WithError(err).Error("Failed execute reset command") + logger.Logger.WithError(err).Error("Failed execute reset command") return } rowsAffected, err := result.RowsAffected() if err != nil { - ls.logger.WithError(err).Error("Failed to get rows affected") + logger.Logger.WithError(err).Error("Failed to get rows affected") return } - ls.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "rowsAffected": rowsAffected, }).Info("Reset router") if err != nil { - ls.logger.WithField("key", key).WithError(err).Error("ResetRouter failed") + logger.Logger.WithField("key", key).WithError(err).Error("ResetRouter failed") } } } @@ -381,7 +379,7 @@ func (ls *LDKService) resetRouterInternal() { func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string) (*lnclient.PayInvoiceResponse, error) { paymentRequest, err := decodepay.Decodepay(invoice) if err != nil { - ls.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "bolt11": invoice, }).Errorf("Failed to decode bolt11 invoice: %v", err) @@ -407,7 +405,7 @@ func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string) (*lnc paymentHash, err := ls.node.Bolt11Payment().Send(invoice) if err != nil { - ls.logger.WithError(err).Error("SendPayment failed") + logger.Logger.WithError(err).Error("SendPayment failed") return nil, err } fee := uint64(0) @@ -420,23 +418,23 @@ func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string) (*lnc eventPaymentFailed, isEventPaymentFailedEvent := (*event).(ldk_node.EventPaymentFailed) if isEventPaymentSuccessfulEvent && eventPaymentSuccessful.PaymentHash == paymentHash { - ls.logger.Info("Got payment success event") + logger.Logger.Info("Got payment success event") payment := ls.node.Payment(paymentHash) if payment == nil { - ls.logger.Errorf("Couldn't find payment by payment hash: %v", paymentHash) + logger.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 { - ls.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "payment": payment, }).Error("Payment is not a bolt11 kind") } if bolt11PaymentKind.Preimage == nil { - ls.logger.Errorf("No payment preimage for payment hash: %v", paymentHash) + logger.Logger.Errorf("No payment preimage for payment hash: %v", paymentHash) return nil, errors.New("Payment preimage not found") } preimage = *bolt11PaymentKind.Preimage @@ -469,7 +467,7 @@ func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string) (*lnc failureReasonMessage = "UnknownError" } - ls.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "paymentHash": paymentHash, "failureReason": failureReason, "failureReasonMessage": failureReasonMessage, @@ -483,7 +481,7 @@ func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string) (*lnc return nil, errors.New("Payment timed out") } - ls.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "duration": time.Since(paymentStart).Milliseconds(), "fee": fee, }).Info("Successful payment") @@ -494,7 +492,7 @@ func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string) (*lnc }, nil } -func (ls *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 uint64, destination, preimage string, custom_records []lnclient.TLVRecord) (preImage string, err error) { paymentStart := time.Now() customTlvs := []ldk_node.TlvEntry{} @@ -508,9 +506,9 @@ func (ls *LDKService) SendKeysend(ctx context.Context, amount int64, destination ldkEventSubscription := ls.ldkEventBroadcaster.Subscribe() defer ls.ldkEventBroadcaster.CancelSubscription(ldkEventSubscription) - paymentHash, err := ls.node.SpontaneousPayment().Send(uint64(amount), destination, customTlvs) + paymentHash, err := ls.node.SpontaneousPayment().Send(amount, destination, customTlvs) if err != nil { - ls.logger.WithError(err).Error("Keysend failed") + logger.Logger.WithError(err).Error("Keysend failed") return "", err } @@ -523,23 +521,23 @@ func (ls *LDKService) SendKeysend(ctx context.Context, amount int64, destination eventPaymentFailed, isEventPaymentFailedEvent := (*event).(ldk_node.EventPaymentFailed) if isEventPaymentSuccessfulEvent && eventPaymentSuccessful.PaymentHash == paymentHash { - ls.logger.Info("Got payment success event") + logger.Logger.Info("Got payment success event") payment := ls.node.Payment(paymentHash) if payment == nil { - ls.logger.Errorf("Couldn't find payment by payment hash: %v", paymentHash) + logger.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 { - ls.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "payment": payment, }).Error("Payment is not a spontaneous kind") } if spontaneousPaymentKind.Preimage == nil { - ls.logger.Errorf("No payment preimage for payment hash: %v", paymentHash) + logger.Logger.Errorf("No payment preimage for payment hash: %v", paymentHash) return "", errors.New("Payment preimage not found") } preimage = *spontaneousPaymentKind.Preimage @@ -572,7 +570,7 @@ func (ls *LDKService) SendKeysend(ctx context.Context, amount int64, destination failureReasonMessage = "UnknownError" } - ls.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "paymentHash": paymentHash, "failureReason": failureReason, "failureReasonMessage": failureReasonMessage, @@ -586,7 +584,7 @@ func (ls *LDKService) SendKeysend(ctx context.Context, amount int64, destination return "", errors.New("keysend payment timed out") } - ls.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "duration": time.Since(paymentStart).Milliseconds(), "fee": fee, }).Info("Successful keysend payment") @@ -628,7 +626,7 @@ func (ls *LDKService) getMaxSpendable() int64 { return int64(spendable) } -func (ls *LDKService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *nip47.Transaction, err error) { +func (ls *LDKService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *lnclient.Transaction, err error) { maxReceivable := ls.getMaxReceivable() @@ -650,14 +648,14 @@ func (ls *LDKService) MakeInvoice(ctx context.Context, amount int64, description uint32(expiry)) if err != nil { - ls.logger.WithError(err).Error("MakeInvoice failed") + logger.Logger.WithError(err).Error("MakeInvoice failed") return nil, err } var expiresAt *int64 paymentRequest, err := decodepay.Decodepay(invoice) if err != nil { - ls.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "bolt11": invoice, }).Errorf("Failed to decode bolt11 invoice: %v", err) @@ -668,7 +666,7 @@ func (ls *LDKService) MakeInvoice(ctx context.Context, amount int64, description description = paymentRequest.Description descriptionHash = paymentRequest.DescriptionHash - transaction = &nip47.Transaction{ + transaction = &lnclient.Transaction{ Type: "incoming", Invoice: invoice, PaymentHash: paymentRequest.PaymentHash, @@ -682,26 +680,26 @@ func (ls *LDKService) MakeInvoice(ctx context.Context, amount int64, description return transaction, nil } -func (ls *LDKService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *nip47.Transaction, err error) { +func (ls *LDKService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *lnclient.Transaction, err error) { payment := ls.node.Payment(paymentHash) if payment == nil { - ls.logger.Errorf("Couldn't find payment by payment hash: %v", paymentHash) + logger.Logger.Errorf("Couldn't find payment by payment hash: %v", paymentHash) return nil, errors.New("Payment not found") } transaction, err = ls.ldkPaymentToTransaction(payment) if err != nil { - ls.logger.Errorf("Failed to map transaction: %v", err) + logger.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 []nip47.Transaction, err error) { - transactions = []nip47.Transaction{} +func (ls *LDKService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []lnclient.Transaction, err error) { + transactions = []lnclient.Transaction{} // TODO: support pagination payments := ls.node.ListPayments() @@ -711,7 +709,7 @@ func (ls *LDKService) ListTransactions(ctx context.Context, from, until, limit, transaction, err := ls.ldkPaymentToTransaction(&payment) if err != nil { - ls.logger.WithError(err).Error("Failed to map transaction") + logger.Logger.WithError(err).Error("Failed to map transaction") continue } @@ -739,7 +737,7 @@ func (ls *LDKService) ListTransactions(ctx context.Context, from, until, limit, if offset < uint64(len(transactions)) { transactions = transactions[offset:] } else { - transactions = []nip47.Transaction{} + transactions = []lnclient.Transaction{} } } @@ -747,7 +745,7 @@ func (ls *LDKService) ListTransactions(ctx context.Context, from, until, limit, transactions = transactions[:limit] } - // ls.logger.WithField("transactions", transactions).Debug("Listed transactions") + // logger.Logger.WithField("transactions", transactions).Debug("Listed transactions") return transactions, nil } @@ -772,7 +770,7 @@ func (ls *LDKService) ListChannels(ctx context.Context) ([]lnclient.Channel, err channels := []lnclient.Channel{} - // gs.logger.WithFields(logrus.Fields{ + // logger.Logger.WithFields(logrus.Fields{ // "channels": ldkChannels, // }).Debug("Listed Channels") @@ -811,7 +809,7 @@ func (ls *LDKService) GetNodeConnectionInfo(ctx context.Context) (nodeConnection } port, err := strconv.Atoi(parts[1]) if err != nil { - gs.logger.WithError(err).Error("ConnectPeer failed") + logger.Logger.WithError(err).Error("ConnectPeer failed") return nil, err }*/ @@ -825,7 +823,7 @@ func (ls *LDKService) GetNodeConnectionInfo(ctx context.Context) (nodeConnection 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 { - ls.logger.WithField("request", connectPeerRequest).WithError(err).Error("ConnectPeer failed") + logger.Logger.WithField("request", connectPeerRequest).WithError(err).Error("ConnectPeer failed") return err } @@ -850,15 +848,15 @@ func (ls *LDKService) OpenChannel(ctx context.Context, openChannelRequest *lncli ldkEventSubscription := ls.ldkEventBroadcaster.Subscribe() defer ls.ldkEventBroadcaster.CancelSubscription(ldkEventSubscription) - ls.logger.WithField("peer_id", foundPeer.NodeId).Info("Opening channel") + logger.Logger.WithField("peer_id", foundPeer.NodeId).Info("Opening channel") userChannelId, err := ls.node.ConnectOpenChannel(foundPeer.NodeId, foundPeer.Address, uint64(openChannelRequest.Amount), nil, nil, openChannelRequest.Public) if err != nil { - ls.logger.WithError(err).Error("OpenChannel failed") + logger.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) - ls.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "peer_id": foundPeer.NodeId, "channel_id": userChannelId, }).Info("Funded channel") @@ -871,7 +869,7 @@ func (ls *LDKService) OpenChannel(ctx context.Context, openChannelRequest *lncli if isChannelClosedEvent { closureReason := ls.getChannelCloseReason(&channelClosedEvent) - ls.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "event": channelClosedEvent, "reason": closureReason, }).Info("Failed to open channel") @@ -892,13 +890,13 @@ func (ls *LDKService) OpenChannel(ctx context.Context, openChannelRequest *lncli } func (ls *LDKService) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) (*lnclient.CloseChannelResponse, error) { - ls.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "request": closeChannelRequest, }).Info("Closing Channel") // TODO: support passing force option err := ls.node.CloseChannel(closeChannelRequest.ChannelId, closeChannelRequest.NodeId, closeChannelRequest.Force) if err != nil { - ls.logger.WithError(err).Error("CloseChannel failed") + logger.Logger.WithError(err).Error("CloseChannel failed") return nil, err } return &lnclient.CloseChannelResponse{}, nil @@ -907,7 +905,7 @@ func (ls *LDKService) CloseChannel(ctx context.Context, closeChannelRequest *lnc func (ls *LDKService) GetNewOnchainAddress(ctx context.Context) (string, error) { address, err := ls.node.OnchainPayment().NewAddress() if err != nil { - ls.logger.WithError(err).Error("NewOnchainAddress failed") + logger.Logger.WithError(err).Error("NewOnchainAddress failed") return "", err } return address, nil @@ -915,7 +913,7 @@ func (ls *LDKService) GetNewOnchainAddress(ctx context.Context) (string, error) func (ls *LDKService) GetOnchainBalance(ctx context.Context) (*lnclient.OnchainBalanceResponse, error) { balances := ls.node.ListBalances() - ls.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "balances": balances, }).Debug("Listed Balances") return &lnclient.OnchainBalanceResponse{ @@ -928,7 +926,7 @@ func (ls *LDKService) GetOnchainBalance(ctx context.Context) (*lnclient.OnchainB func (ls *LDKService) RedeemOnchainFunds(ctx context.Context, toAddress string) (string, error) { txId, err := ls.node.OnchainPayment().SendAllToAddress(toAddress) if err != nil { - ls.logger.WithError(err).Error("SendAllToOnchainAddress failed") + logger.Logger.WithError(err).Error("SendAllToOnchainAddress failed") return "", err } return txId, nil @@ -943,15 +941,15 @@ func (ls *LDKService) ResetRouter(key string) error { func (ls *LDKService) SignMessage(ctx context.Context, message string) (string, error) { sign, err := ls.node.SignMessage([]byte(message)) if err != nil { - ls.logger.Errorf("SignMessage failed: %v", err) + logger.Logger.Errorf("SignMessage failed: %v", err) return "", err } return sign, nil } -func (ls *LDKService) ldkPaymentToTransaction(payment *ldk_node.PaymentDetails) (*nip47.Transaction, error) { - // gs.logger.WithField("payment", payment).Debug("Mapping LDK payment to transaction") +func (ls *LDKService) ldkPaymentToTransaction(payment *ldk_node.PaymentDetails) (*lnclient.Transaction, error) { + // logger.Logger.WithField("payment", payment).Debug("Mapping LDK payment to transaction") transactionType := "incoming" if payment.Direction == ldk_node.PaymentDirectionOutbound { @@ -974,7 +972,7 @@ func (ls *LDKService) ldkPaymentToTransaction(payment *ldk_node.PaymentDetails) bolt11Invoice = *bolt11PaymentKind.Bolt11Invoice paymentRequest, err := decodepay.Decodepay(strings.ToLower(bolt11Invoice)) if err != nil { - ls.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "bolt11": bolt11Invoice, }).Errorf("Failed to decode bolt11 invoice: %v", err) @@ -1028,7 +1026,7 @@ func (ls *LDKService) ldkPaymentToTransaction(payment *ldk_node.PaymentDetails) fee = *payment.FeeMsat } - return &nip47.Transaction{ + return &lnclient.Transaction{ Type: transactionType, Preimage: preimage, PaymentHash: paymentHash, @@ -1047,7 +1045,7 @@ func (ls *LDKService) ldkPaymentToTransaction(payment *ldk_node.PaymentDetails) func (ls *LDKService) SendPaymentProbes(ctx context.Context, invoice string) error { err := ls.node.Bolt11Payment().SendProbes(invoice) if err != nil { - ls.logger.Errorf("Bolt11Payment.SendProbes failed: %v", err) + logger.Logger.Errorf("Bolt11Payment.SendProbes failed: %v", err) return err } @@ -1057,7 +1055,7 @@ func (ls *LDKService) SendPaymentProbes(ctx context.Context, invoice string) err func (ls *LDKService) SendSpontaneousPaymentProbes(ctx context.Context, amountMsat uint64, nodeId string) error { err := ls.node.SpontaneousPayment().SendProbes(amountMsat, nodeId) if err != nil { - ls.logger.Errorf("SpontaneousPayment.SendProbes failed: %v", err) + logger.Logger.Errorf("SpontaneousPayment.SendProbes failed: %v", err) return err } @@ -1123,7 +1121,7 @@ func (ls *LDKService) GetLogOutput(ctx context.Context, maxLen int) ([]byte, err allLogFiles, err := filepath.Glob(filepath.Join(logPath, "ldk_node_*.log")) if err != nil { - ls.logger.WithError(err).Error("GetLogOutput failed to list log files") + logger.Logger.WithError(err).Error("GetLogOutput failed to list log files") return nil, err } @@ -1137,7 +1135,7 @@ func (ls *LDKService) GetLogOutput(ctx context.Context, maxLen int) ([]byte, err logData, err := utils.ReadFileTail(lastLogFileName, maxLen) if err != nil { - ls.logger.WithError(err).Error("GetLogOutput failed to read log file") + logger.Logger.WithError(err).Error("GetLogOutput failed to read log file") return nil, err } @@ -1145,7 +1143,7 @@ func (ls *LDKService) GetLogOutput(ctx context.Context, maxLen int) ([]byte, err } func (ls *LDKService) handleLdkEvent(event *ldk_node.Event) { - ls.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "event": event, }).Info("Received LDK event") @@ -1162,7 +1160,7 @@ func (ls *LDKService) handleLdkEvent(event *ldk_node.Event) { ls.publishChannelsBackupEvent() case ldk_node.EventChannelClosed: closureReason := ls.getChannelCloseReason(&eventType) - ls.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "event": event, "reason": closureReason, }).Info("Channel closed") @@ -1219,7 +1217,7 @@ func (ls *LDKService) publishChannelsBackupEvent() { func (ls *LDKService) GetBalances(ctx context.Context) (*lnclient.BalancesResponse, error) { onchainBalance, err := ls.GetOnchainBalance(ctx) if err != nil { - ls.logger.WithError(err).Error("Failed to retrieve onchain balance") + logger.Logger.WithError(err).Error("Failed to retrieve onchain balance") return nil, err } @@ -1267,11 +1265,11 @@ func (ls *LDKService) GetStorageDir() (string, error) { return "ldk/storage", nil } -func deleteOldLDKLogs(logger *logrus.Logger, ldkLogDir string) { - logger.WithField("ldkLogDir", ldkLogDir).Info("Deleting old LDK logs") +func deleteOldLDKLogs(ldkLogDir string) { + logger.Logger.WithField("ldkLogDir", ldkLogDir).Info("Deleting old LDK logs") files, err := os.ReadDir(ldkLogDir) if err != nil { - logger.WithField("path", ldkLogDir).WithError(err).Error("Failed to list ldk log directory") + logger.Logger.WithField("path", ldkLogDir).WithError(err).Error("Failed to list ldk log directory") return } @@ -1281,17 +1279,17 @@ func deleteOldLDKLogs(logger *logrus.Logger, ldkLogDir string) { filePath := filepath.Join(ldkLogDir, file.Name()) fileInfo, err := file.Info() if err != nil { - logger.WithField("filePath", filePath).WithError(err).Error("Failed to get file info") + logger.Logger.WithField("filePath", filePath).WithError(err).Error("Failed to get file info") continue } // delete files last modified over 3 days ago if fileInfo.ModTime().Before(time.Now().AddDate(0, 0, -3)) { err := os.Remove(filePath) if err != nil { - logger.WithField("filePath", filePath).WithError(err).Error("Failed to get file info") + logger.Logger.WithField("filePath", filePath).WithError(err).Error("Failed to get file info") continue } - logger.WithField("filePath", filePath).Info("Deleted old LDK log file") + logger.Logger.WithField("filePath", filePath).Info("Deleted old LDK log file") } } } diff --git a/lnclient/ldk/ldk_event_broadcaster.go b/lnclient/ldk/ldk_event_broadcaster.go index a5468fa22..c0b35e6c7 100644 --- a/lnclient/ldk/ldk_event_broadcaster.go +++ b/lnclient/ldk/ldk_event_broadcaster.go @@ -6,6 +6,8 @@ import ( "time" "github.com/getAlby/ldk-node-go/ldk_node" + "github.com/getAlby/nostr-wallet-connect/logger" + // "github.com/getAlby/nostr-wallet-connect/ldk_node" "github.com/sirupsen/logrus" ) @@ -20,7 +22,6 @@ There are 3 main channels: Based on https://betterprogramming.pub/how-to-broadcast-messages-in-go-using-channels-b68f42bdf32e */ type ldkEventBroadcastServer struct { - logger *logrus.Logger source <-chan *ldk_node.Event listeners []chan *ldk_node.Event addListener chan chan *ldk_node.Event @@ -32,9 +33,8 @@ type LDKEventBroadcaster interface { CancelSubscription(chan *ldk_node.Event) } -func NewLDKEventBroadcaster(logger *logrus.Logger, ctx context.Context, source <-chan *ldk_node.Event) LDKEventBroadcaster { +func NewLDKEventBroadcaster(ctx context.Context, source <-chan *ldk_node.Event) LDKEventBroadcaster { service := &ldkEventBroadcastServer{ - logger: logger, source: source, listeners: make([]chan *ldk_node.Event, 0), addListener: make(chan chan *ldk_node.Event), @@ -56,7 +56,7 @@ func (s *ldkEventBroadcastServer) CancelSubscription(channel chan *ldk_node.Even func() { defer func() { if r := recover(); r != nil { - s.logger.WithField("r", r).Error("Failed to close subscription channel") + logger.Logger.WithField("r", r).Error("Failed to close subscription channel") } }() close(channel) @@ -72,7 +72,7 @@ func (s *ldkEventBroadcastServer) serve(ctx context.Context) { func() { defer func() { if r := recover(); r != nil { - s.logger.WithField("r", r).Error("Failed to close subscription channel") + logger.Logger.WithField("r", r).Error("Failed to close subscription channel") } }() close(listener) @@ -101,7 +101,7 @@ func (s *ldkEventBroadcastServer) serve(ctx context.Context) { } case event := <-s.source: // got a new LDK event - send it to all listeners - s.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "event": event, "listenerCount": len(s.listeners), }).Debug("Sending LDK event to listeners") @@ -110,7 +110,7 @@ func (s *ldkEventBroadcastServer) serve(ctx context.Context) { // if we fail to send the event to the listener it was probably closed defer func() { if r := recover(); r != nil { - s.logger.WithField("r", r).Error("Failed to send event to listener") + logger.Logger.WithField("r", r).Error("Failed to send event to listener") } }() @@ -119,11 +119,11 @@ func (s *ldkEventBroadcastServer) serve(ctx context.Context) { // worst case scenario: it times out because the listener is stuck processing an event select { case listener <- event: - s.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "event": event, }).Debug("Sent LDK event to listener") case <-time.After(5 * time.Second): - s.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "event": event, }).Error("Timeout sending LDK event to listener") } diff --git a/lnclient/lnd/lnd.go b/lnclient/lnd/lnd.go index bf82b6c20..1e13f7d2c 100644 --- a/lnclient/lnd/lnd.go +++ b/lnclient/lnd/lnd.go @@ -17,7 +17,7 @@ import ( "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/getAlby/nostr-wallet-connect/logger" "github.com/sirupsen/logrus" // "gorm.io/gorm" @@ -29,8 +29,6 @@ import ( // todo: drop dependency on lndhub package type LNDService struct { client *wrapper.LNDWrapper - // db *gorm.DB - Logger *logrus.Logger } func (svc *LNDService) GetBalance(ctx context.Context) (balance int64, err error) { @@ -41,7 +39,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 []nip47.Transaction, err error) { +func (svc *LNDService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []lnclient.Transaction, err error) { // Fetch invoices var invoices []*lnrpc.Invoice if invoiceType == "" || invoiceType == "incoming" { @@ -83,7 +81,7 @@ func (svc *LNDService) ListTransactions(ctx context.Context, from, until, limit, if payment.PaymentRequest != "" { paymentRequest, err = decodepay.Decodepay(strings.ToLower(payment.PaymentRequest)) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "bolt11": payment.PaymentRequest, }).Errorf("Failed to decode bolt11 invoice: %v", err) @@ -102,7 +100,7 @@ func (svc *LNDService) ListTransactions(ctx context.Context, from, until, limit, settledAt = &settledAtUnix } - transaction := nip47.Transaction{ + transaction := lnclient.Transaction{ Type: "outgoing", Invoice: payment.PaymentRequest, Preimage: payment.PaymentPreimage, @@ -245,14 +243,14 @@ 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 *nip47.Transaction, err error) { +func (svc *LNDService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *lnclient.Transaction, err error) { var descriptionHashBytes []byte if descriptionHash != "" { descriptionHashBytes, err = hex.DecodeString(descriptionHash) if err != nil || len(descriptionHashBytes) != 32 { - svc.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "amount": amount, "description": description, "descriptionHash": descriptionHash, @@ -276,11 +274,11 @@ func (svc *LNDService) MakeInvoice(ctx context.Context, amount int64, descriptio return transaction, nil } -func (svc *LNDService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *nip47.Transaction, err error) { +func (svc *LNDService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *lnclient.Transaction, err error) { paymentHashBytes, err := hex.DecodeString(paymentHash) if err != nil || len(paymentHashBytes) != 32 { - svc.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "paymentHash": paymentHash, }).Errorf("Invalid payment hash") return nil, errors.New("Payment hash must be 32 bytes hex") @@ -320,7 +318,7 @@ func (svc *LNDService) SendPaymentSync(ctx context.Context, payReq string) (*lnc }, nil } -func (svc *LNDService) SendKeysend(ctx context.Context, amount int64, destination, preimage string, custom_records []lnclient.TLVRecord) (respPreimage string, err error) { +func (svc *LNDService) SendKeysend(ctx context.Context, amount uint64, destination, preimage string, custom_records []lnclient.TLVRecord) (respPreimage string, err error) { destBytes, err := hex.DecodeString(destination) if err != nil { return "", err @@ -334,7 +332,7 @@ func (svc *LNDService) SendKeysend(ctx context.Context, amount int64, destinatio preImageBytes, err = hex.DecodeString(preimage) } if err != nil || len(preImageBytes) != 32 { - svc.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "amount": amount, "destination": destination, "preimage": preimage, @@ -357,7 +355,7 @@ func (svc *LNDService) SendKeysend(ctx context.Context, amount int64, destinatio destCustomRecords[KEYSEND_CUSTOM_RECORD] = preImageBytes sendPaymentRequest := &lnrpc.SendRequest{ Dest: destBytes, - AmtMsat: amount, + AmtMsat: int64(amount), PaymentHash: paymentHashBytes, DestFeatures: []lnrpc.FeatureBit{lnrpc.FeatureBit_TLV_ONION_REQ}, DestCustomRecords: destCustomRecords, @@ -365,7 +363,7 @@ func (svc *LNDService) SendKeysend(ctx context.Context, amount int64, destinatio resp, err := svc.client.SendPaymentSync(ctx, sendPaymentRequest) if err != nil { - svc.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "amount": amount, "payeePubkey": destination, "paymentHash": paymentHashHex, @@ -376,7 +374,7 @@ func (svc *LNDService) SendKeysend(ctx context.Context, amount int64, destinatio return "", err } if resp.PaymentError != "" { - svc.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "amount": amount, "payeePubkey": destination, "paymentHash": paymentHashHex, @@ -388,7 +386,7 @@ func (svc *LNDService) SendKeysend(ctx context.Context, amount int64, destinatio } respPreimage = hex.EncodeToString(resp.PaymentPreimage) if respPreimage == "" { - svc.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "amount": amount, "payeePubkey": destination, "paymentHash": paymentHashHex, @@ -398,7 +396,7 @@ func (svc *LNDService) SendKeysend(ctx context.Context, amount int64, destinatio }).Errorf("No preimage in keysend response") return "", errors.New("no preimage in keysend response") } - svc.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "amount": amount, "payeePubkey": destination, "paymentHash": paymentHashHex, @@ -419,7 +417,7 @@ func makePreimageHex() ([]byte, error) { return bytes, nil } -func NewLNDService(ctx context.Context, logger *logrus.Logger, lndAddress, lndCertHex, lndMacaroonHex string) (result lnclient.LNClient, err error) { +func NewLNDService(ctx context.Context, 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") } @@ -430,7 +428,7 @@ func NewLNDService(ctx context.Context, logger *logrus.Logger, lndAddress, lndCe MacaroonHex: lndMacaroonHex, }) if err != nil { - logger.Errorf("Failed to create new LND client %v", err) + logger.Logger.Errorf("Failed to create new LND client %v", err) return nil, err } info, err := lndClient.GetInfo(ctx, &lnrpc.GetInfoRequest{}) @@ -438,9 +436,9 @@ func NewLNDService(ctx context.Context, logger *logrus.Logger, lndAddress, lndCe return nil, err } - lndService := &LNDService{client: lndClient, Logger: logger} + lndService := &LNDService{client: lndClient} - logger.Infof("Connected to LND - alias %s", info.Alias) + logger.Logger.Infof("Connected to LND - alias %s", info.Alias) return lndService, nil } @@ -487,7 +485,7 @@ func (svc *LNDService) OpenChannel(ctx context.Context, openChannelRequest *lncl return nil, errors.New("node is not peered yet") } - svc.Logger.WithField("peer_id", foundPeer.NodeId).Info("Opening channel") + logger.Logger.WithField("peer_id", foundPeer.NodeId).Info("Opening channel") nodePub, err := hex.DecodeString(openChannelRequest.Pubkey) if err != nil { @@ -516,7 +514,7 @@ func (svc *LNDService) OpenChannel(ctx context.Context, openChannelRequest *lncl } func (svc *LNDService) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) (*lnclient.CloseChannelResponse, error) { - svc.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "request": closeChannelRequest, }).Info("Closing Channel") @@ -564,7 +562,7 @@ func (svc *LNDService) CloseChannel(ctx context.Context, closeChannelRequest *ln if err != nil { return nil, err } - svc.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "closingTxid": txid.String(), }).Info("Channel close pending") // TODO: return the closing tx id or fire an event @@ -578,7 +576,7 @@ func (svc *LNDService) GetNewOnchainAddress(ctx context.Context) (string, error) Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH, }) if err != nil { - svc.Logger.WithError(err).Error("NewOnchainAddress failed") + logger.Logger.WithError(err).Error("NewOnchainAddress failed") return "", err } return resp.Address, nil @@ -589,7 +587,7 @@ func (svc *LNDService) GetOnchainBalance(ctx context.Context) (*lnclient.Onchain if err != nil { return nil, err } - svc.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "balances": balances, }).Debug("Listed Balances") return &lnclient.OnchainBalanceResponse{ @@ -641,7 +639,7 @@ func (svc *LNDService) SignMessage(ctx context.Context, message string) (string, func (svc *LNDService) GetBalances(ctx context.Context) (*lnclient.BalancesResponse, error) { onchainBalance, err := svc.GetOnchainBalance(ctx) if err != nil { - svc.Logger.WithError(err).Error("Failed to retrieve onchain balance") + logger.Logger.WithError(err).Error("Failed to retrieve onchain balance") return nil, err } @@ -681,7 +679,7 @@ func (svc *LNDService) GetBalances(ctx context.Context) (*lnclient.BalancesRespo }, nil } -func lndInvoiceToTransaction(invoice *lnrpc.Invoice) *nip47.Transaction { +func lndInvoiceToTransaction(invoice *lnrpc.Invoice) *lnclient.Transaction { var settledAt *int64 var preimage string if invoice.State == lnrpc.Invoice_SETTLED { @@ -695,7 +693,7 @@ func lndInvoiceToTransaction(invoice *lnrpc.Invoice) *nip47.Transaction { expiresAt = &expiresAtUnix } - return &nip47.Transaction{ + return &lnclient.Transaction{ Type: "incoming", Invoice: invoice.PaymentRequest, Description: invoice.Memo, diff --git a/lnclient/models.go b/lnclient/models.go index 4a5e81ed1..bb8c0f986 100644 --- a/lnclient/models.go +++ b/lnclient/models.go @@ -1,4 +1,3 @@ -// TODO: move to lnclient/models.go package lnclient import ( @@ -43,7 +42,7 @@ type NodeConnectionInfo struct { type LNClient interface { SendPaymentSync(ctx context.Context, payReq string) (*PayInvoiceResponse, error) - SendKeysend(ctx context.Context, amount int64, destination, preimage string, customRecords []TLVRecord) (preImage string, err error) + SendKeysend(ctx context.Context, amount uint64, 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) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *Transaction, err error) diff --git a/lnclient/phoenixd/phoenixd.go b/lnclient/phoenixd/phoenixd.go index 49ac69dec..177776eb4 100644 --- a/lnclient/phoenixd/phoenixd.go +++ b/lnclient/phoenixd/phoenixd.go @@ -13,7 +13,7 @@ import ( "time" "github.com/getAlby/nostr-wallet-connect/lnclient" - "github.com/getAlby/nostr-wallet-connect/nip47" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/sirupsen/logrus" ) @@ -67,12 +67,11 @@ type BalanceResponse struct { type PhoenixService struct { Address string Authorization string - Logger *logrus.Logger } -func NewPhoenixService(logger *logrus.Logger, address string, authorization string) (result lnclient.LNClient, err error) { +func NewPhoenixService(address string, authorization string) (result lnclient.LNClient, err error) { authorizationBase64 := b64.StdEncoding.EncodeToString([]byte(":" + authorization)) - phoenixService := &PhoenixService{Logger: logger, Address: address, Authorization: authorizationBase64} + phoenixService := &PhoenixService{Address: address, Authorization: authorizationBase64} return phoenixService, nil } @@ -120,7 +119,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 []nip47.Transaction, err error) { +func (svc *PhoenixService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []lnclient.Transaction, err error) { incomingQuery := url.Values{} if from != 0 { incomingQuery.Add("from", strconv.FormatUint(from*1000, 10)) @@ -138,7 +137,7 @@ func (svc *PhoenixService) ListTransactions(ctx context.Context, from, until, li incomingUrl := svc.Address + "/payments/incoming?" + incomingQuery.Encode() - svc.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "url": incomingUrl, }).Infof("Fetching incoming tranasctions: %s", incomingUrl) incomingReq, err := http.NewRequest(http.MethodGet, incomingUrl, nil) @@ -158,14 +157,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 = []nip47.Transaction{} + transactions = []lnclient.Transaction{} for _, invoice := range incomingPayments { var settledAt *int64 if invoice.CompletedAt != 0 { settledAtUnix := time.UnixMilli(invoice.CompletedAt).Unix() settledAt = &settledAtUnix } - transaction := nip47.Transaction{ + transaction := lnclient.Transaction{ Type: "incoming", Invoice: invoice.Invoice, Preimage: invoice.Preimage, @@ -197,7 +196,7 @@ func (svc *PhoenixService) ListTransactions(ctx context.Context, from, until, li outgoingUrl := svc.Address + "/payments/outgoing?" + outgoingQuery.Encode() - svc.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "url": outgoingUrl, }).Infof("Fetching outgoing tranasctions: %s", outgoingUrl) outgoingReq, err := http.NewRequest(http.MethodGet, outgoingUrl, nil) @@ -221,7 +220,7 @@ func (svc *PhoenixService) ListTransactions(ctx context.Context, from, until, li settledAtUnix := time.UnixMilli(invoice.CompletedAt).Unix() settledAt = &settledAtUnix } - transaction := nip47.Transaction{ + transaction := lnclient.Transaction{ Type: "outgoing", Invoice: invoice.Invoice, Preimage: invoice.Preimage, @@ -274,7 +273,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 *nip47.Transaction, err error) { +func (svc *PhoenixService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *lnclient.Transaction, err error) { form := url.Values{} amountSat := strconv.FormatInt(amount/1000, 10) form.Add("amountSat", amountSat) @@ -288,7 +287,7 @@ func (svc *PhoenixService) MakeInvoice(ctx context.Context, amount int64, descri today := time.Now().UTC().Format("2006-02-01") // querying is too slow so we limit the invoices we query with the date - see list transactions form.Add("externalId", today) // for some resone phoenixd requires an external id to query a list of invoices. thus we set this to nwc - svc.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "externalId": today, "amountSat": amountSat, }).Infof("Requesting phoenix invoice") @@ -311,7 +310,7 @@ func (svc *PhoenixService) MakeInvoice(ctx context.Context, amount int64, descri } expiresAt := time.Now().Add(1 * time.Hour).Unix() - tx := &nip47.Transaction{ + tx := &lnclient.Transaction{ Type: "incoming", Invoice: invoiceRes.Serialized, Preimage: "", @@ -325,7 +324,7 @@ func (svc *PhoenixService) MakeInvoice(ctx context.Context, amount int64, descri return tx, nil } -func (svc *PhoenixService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *nip47.Transaction, err error) { +func (svc *PhoenixService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *lnclient.Transaction, err error) { req, err := http.NewRequest(http.MethodGet, svc.Address+"/payments/incoming/"+paymentHash, nil) if err != nil { return nil, err @@ -348,7 +347,7 @@ func (svc *PhoenixService) LookupInvoice(ctx context.Context, paymentHash string settledAtUnix := time.UnixMilli(invoiceRes.CompletedAt).Unix() settledAt = &settledAtUnix } - transaction = &nip47.Transaction{ + transaction = &lnclient.Transaction{ Type: "incoming", Invoice: invoiceRes.Invoice, Preimage: invoiceRes.Preimage, @@ -390,7 +389,7 @@ func (svc *PhoenixService) SendPaymentSync(ctx context.Context, payReq string) ( }, nil } -func (svc *PhoenixService) SendKeysend(ctx context.Context, amount int64, destination, preimage string, custom_records []lnclient.TLVRecord) (respPreimage string, err error) { +func (svc *PhoenixService) SendKeysend(ctx context.Context, amount uint64, destination, preimage string, custom_records []lnclient.TLVRecord) (respPreimage string, err error) { return "", errors.New("not implemented") } diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 000000000..971173774 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,52 @@ +package logger + +import ( + "os" + "path/filepath" + "strconv" + + "github.com/orandin/lumberjackrus" + "github.com/sirupsen/logrus" +) + +const ( + logDir = "log" + logFilename = "nwc.log" +) + +var Logger *logrus.Logger +var logFilePath string + +func Init(logLevel string) { + Logger = logrus.New() + Logger.SetFormatter(&logrus.JSONFormatter{}) + Logger.SetOutput(os.Stdout) + logrusLogLevel, err := strconv.Atoi(logLevel) + if err != nil { + logrusLogLevel = int(logrus.InfoLevel) + } + Logger.SetLevel(logrus.Level(logrusLogLevel)) +} + +func AddFileLogger(workdir string) error { + logFilePath = filepath.Join(workdir, logDir, logFilename) + fileLoggerHook, err := lumberjackrus.NewHook( + &lumberjackrus.LogFile{ + Filename: logFilePath, + MaxAge: 3, + MaxBackups: 3, + }, + logrus.InfoLevel, + &logrus.JSONFormatter{}, + nil, + ) + if err != nil { + return err + } + Logger.AddHook(fileLoggerHook) + return nil +} + +func GetLogFilePath() string { + return logFilePath +} diff --git a/lsp/models.go b/lsp/models.go index a7713c0b2..0c1a675eb 100644 --- a/lsp/models.go +++ b/lsp/models.go @@ -1,9 +1,5 @@ package lsp -import ( - "context" -) - type LSP struct { Pubkey string Url string @@ -16,11 +12,6 @@ const ( LSP_TYPE_LSPS1 = "LSPS1" ) -type NewInstantChannelResponse struct { - FeeAmountMsat uint64 `json:"fee_amount_msat"` - Invoice string `json:"invoice"` -} - func VoltageLSP() LSP { lsp := LSP{ Pubkey: "03aefa43fbb4009b21a4129d05953974b7dbabbbfb511921410080860fca8ee1f0", @@ -91,18 +82,3 @@ func MegalithLSP() 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"` - Public bool `json:"public"` -} - -type NewInstantChannelInvoiceResponse struct { - Invoice string `json:"invoice"` - Fee uint64 `json:"fee"` -} diff --git a/main_http.go b/main_http.go deleted file mode 100644 index 996cd7311..000000000 --- a/main_http.go +++ /dev/null @@ -1,52 +0,0 @@ -//go:build !wails || http -// +build !wails http - -// (http tag above is simply to fix go language server issue and is not needed to build the app) - -package main - -import ( - "context" - "fmt" - 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" -) - -// ignore this warning: we use build tags -// this function will only be executed if no wails tag is set -func main() { - log.Info("NWC Starting in HTTP mode") - ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, os.Kill) - svc, _ := NewService(ctx) - - echologrus.Logger = svc.logger - e := echo.New() - - //register shared routes - 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.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...") - 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.wg.Wait() - svc.logger.Info("Service exited") -} diff --git a/main_wails.go b/main_wails.go index 4ae8dec59..143fb6444 100644 --- a/main_wails.go +++ b/main_wails.go @@ -5,25 +5,32 @@ package main import ( "context" + "embed" + "github.com/getAlby/nostr-wallet-connect/logger" + "github.com/getAlby/nostr-wallet-connect/service" + "github.com/getAlby/nostr-wallet-connect/wails" log "github.com/sirupsen/logrus" ) -// ignore this warning: we use build tags -// this function will only be executed if the wails tag is set +//go:embed all:frontend/dist +var assets embed.FS + +//go:embed appicon.png +var appIcon []byte + func main() { log.Info("NWC Starting in WAILS mode") ctx, cancel := context.WithCancel(context.Background()) - svc, _ := NewService(ctx) + svc, _ := service.NewService(ctx) - app := NewApp(svc) - LaunchWailsApp(app) - svc.logger.Info("Wails app exited") + app := wails.NewApp(svc) + wails.LaunchWailsApp(app, assets, appIcon) + logger.Logger.Info("Wails app exited") - svc.logger.Info("Cancelling service context...") + logger.Logger.Info("Cancelling service context...") // cancel the service context cancel() - svc.logger.Info("Waiting for service to exit...") - svc.wg.Wait() - svc.logger.Info("Service exited") + svc.WaitShutdown() + logger.Logger.Info("Service exited") } diff --git a/migrations/202403171120_delete_ldk_payments.go b/migrations/202403171120_delete_ldk_payments.go deleted file mode 100644 index 2f8c85a9f..000000000 --- a/migrations/202403171120_delete_ldk_payments.go +++ /dev/null @@ -1,51 +0,0 @@ -package migrations - -import ( - _ "embed" - "errors" - "os" - "path/filepath" - - "database/sql" - - "github.com/getAlby/nostr-wallet-connect/config" - "github.com/go-gormigrate/gormigrate/v2" - "github.com/sirupsen/logrus" - "gorm.io/gorm" -) - -// Delete LDK payments that were not migrated to the new LDK format (PaymentKind) -// TODO: delete this sometime in the future (only affects current testers) -func _202403171120_delete_ldk_payments(appConfig *config.AppConfig, logger *logrus.Logger) *gormigrate.Migration { - return &gormigrate.Migration{ - ID: "202403171120_delete_ldk_payments", - Migrate: func(tx *gorm.DB) error { - - ldkDbPath := filepath.Join(appConfig.Workdir, "ldk", "storage", "ldk_node_data.sqlite") - if _, err := os.Stat(ldkDbPath); errors.Is(err, os.ErrNotExist) { - logger.Info("No LDK database, skipping migration") - return nil - } - ldkDb, err := sql.Open("sqlite", ldkDbPath) - if err != nil { - return err - } - result, err := ldkDb.Exec(`update ldk_node_data set primary_namespace="payments_bkp" where primary_namespace == "payments"`) - if err != nil { - return err - } - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - logger.WithFields(logrus.Fields{ - "rowsAffected": rowsAffected, - }).Info("Removed incompatible payments from LDK database") - - return err - }, - Rollback: func(tx *gorm.DB) error { - return nil - }, - } -} diff --git a/migrations/migrate.go b/migrations/migrate.go deleted file mode 100644 index 276567e62..000000000 --- a/migrations/migrate.go +++ /dev/null @@ -1,22 +0,0 @@ -package migrations - -import ( - "github.com/getAlby/nostr-wallet-connect/config" - "github.com/go-gormigrate/gormigrate/v2" - "github.com/sirupsen/logrus" - "gorm.io/gorm" -) - -func Migrate(db *gorm.DB, appConfig *config.AppConfig, logger *logrus.Logger) error { - - m := gormigrate.New(db, gormigrate.DefaultOptions, []*gormigrate.Migration{ - _202401191539_initial_migration, - _202403171120_delete_ldk_payments(appConfig, logger), - _202404021909_nullable_expires_at, - _202405302121_store_decrypted_request, - _202406061259_delete_content, - _202406071726_vacuum, - }) - - return m.Migrate() -} diff --git a/nip47/controllers/decode_request.go b/nip47/controllers/decode_request.go new file mode 100644 index 000000000..2fba4c073 --- /dev/null +++ b/nip47/controllers/decode_request.go @@ -0,0 +1,25 @@ +package controllers + +import ( + "encoding/json" + + "github.com/getAlby/nostr-wallet-connect/logger" + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/sirupsen/logrus" +) + +func decodeRequest(request *models.Request, methodParams interface{}) *models.Response { + err := json.Unmarshal(request.Params, methodParams) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "request": request, + }).WithError(err).Error("Failed to decode NIP-47 request") + return &models.Response{ + ResultType: request.Method, + Error: &models.Error{ + Code: models.ERROR_BAD_REQUEST, + Message: err.Error(), + }} + } + return nil +} diff --git a/nip47/controllers/get_balance_controller.go b/nip47/controllers/get_balance_controller.go new file mode 100644 index 000000000..a9a4f8880 --- /dev/null +++ b/nip47/controllers/get_balance_controller.go @@ -0,0 +1,79 @@ +package controllers + +import ( + "context" + + "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/logger" + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/nbd-wtf/go-nostr" + "github.com/sirupsen/logrus" +) + +const ( + MSAT_PER_SAT = 1000 +) + +type getBalanceResponse struct { + Balance int64 `json:"balance"` + // MaxAmount int `json:"max_amount"` + // BudgetRenewal string `json:"budget_renewal"` +} + +type getBalanceController struct { + lnClient lnclient.LNClient +} + +func NewGetBalanceController(lnClient lnclient.LNClient) *getBalanceController { + return &getBalanceController{ + lnClient: lnClient, + } +} + +// TODO: remove checkPermission - can it be a middleware? +func (controller *getBalanceController) HandleGetBalanceEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, checkPermission checkPermissionFunc, publishResponse publishFunc) { + // basic permissions check + resp := checkPermission(0) + if resp != nil { + publishResponse(resp, nostr.Tags{}) + return + } + + logger.Logger.WithFields(logrus.Fields{ + "request_event_id": requestEventId, + }).Info("Getting balance") + + balance, err := controller.lnClient.GetBalance(ctx) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "request_event_id": requestEventId, + }).WithError(err).Error("Failed to fetch balance") + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: models.ERROR_INTERNAL, + Message: err.Error(), + }, + }, nostr.Tags{}) + return + } + + responsePayload := &getBalanceResponse{ + Balance: balance, + } + + // this is not part of the spec and does not seem to be used + /*appPermission := db.AppPermission{} + controller.db.Where("app_id = ? AND request_method = ?", app.ID, models.PAY_INVOICE_METHOD).First(&appPermission) + + maxAmount := appPermission.MaxAmount + if maxAmount > 0 { + responsePayload.MaxAmount = maxAmount * MSAT_PER_SAT + responsePayload.BudgetRenewal = appPermission.BudgetRenewal + }*/ + + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Result: responsePayload, + }, nostr.Tags{}) +} diff --git a/nip47/controllers/get_balance_controller_test.go b/nip47/controllers/get_balance_controller_test.go new file mode 100644 index 000000000..a2ab671fe --- /dev/null +++ b/nip47/controllers/get_balance_controller_test.go @@ -0,0 +1,109 @@ +package controllers + +import ( + "context" + "encoding/json" + "testing" + + "github.com/nbd-wtf/go-nostr" + "github.com/stretchr/testify/assert" + + "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/getAlby/nostr-wallet-connect/tests" +) + +const nip47GetBalanceJson = ` +{ + "method": "get_balance" +} +` + +func TestHandleGetBalanceEvent_NoPermission(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47GetBalanceJson), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + checkPermission := func(amountMsat uint64) *models.Response { + return &models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: models.ERROR_RESTRICTED, + }, + } + } + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + NewGetBalanceController(svc.LNClient). + HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, checkPermission, publishResponse) + + assert.Nil(t, publishedResponse.Result) + assert.Equal(t, models.ERROR_RESTRICTED, publishedResponse.Error.Code) +} + +func TestHandleGetBalanceEvent_WithPermission(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47GetBalanceJson), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + checkPermission := func(amountMsat uint64) *models.Response { + return nil + } + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + NewGetBalanceController(svc.LNClient). + HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, checkPermission, publishResponse) + + assert.Equal(t, int64(21000), publishedResponse.Result.(*getBalanceResponse).Balance) + assert.Nil(t, publishedResponse.Error) +} + +// create pay_invoice permission +// maxAmount := 1000 +// budgetRenewal := "never" +// appPermission = &db.AppPermission{ +// AppId: app.ID, +// App: *app, +// RequestMethod: models.PAY_INVOICE_METHOD, +// MaxAmount: maxAmount, +// BudgetRenewal: budgetRenewal, +// ExpiresAt: &expiresAt, +// } +// err = svc.DB.Create(appPermission).Error +// assert.NoError(t, err) + +// reqEvent.ID = "test_get_balance_with_budget" +// responses = []*models.Response{} +// svc.nip47Svc.HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent, app, publishResponse) + +// assert.Equal(t, int64(21000), responses[0].Result.(*getBalanceResponse).Balance) +// assert.Equal(t, 1000000, responses[0].Result.(*getBalanceResponse).MaxAmount) +// assert.Equal(t, "never", responses[0].Result.(*getBalanceResponse).BudgetRenewal) diff --git a/nip47/controllers/get_info_controller.go b/nip47/controllers/get_info_controller.go new file mode 100644 index 000000000..add1a6808 --- /dev/null +++ b/nip47/controllers/get_info_controller.go @@ -0,0 +1,85 @@ +package controllers + +import ( + "context" + + "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/logger" + "github.com/getAlby/nostr-wallet-connect/nip47/models" + permissions "github.com/getAlby/nostr-wallet-connect/nip47/permissions" + "github.com/nbd-wtf/go-nostr" + "github.com/sirupsen/logrus" +) + +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 getInfoController struct { + lnClient lnclient.LNClient + permissionsService permissions.PermissionsService +} + +func NewGetInfoController(permissionsService permissions.PermissionsService, lnClient lnclient.LNClient) *getInfoController { + return &getInfoController{ + permissionsService: permissionsService, + lnClient: lnClient, + } +} + +func (controller *getInfoController) HandleGetInfoEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, app *db.App, checkPermission checkPermissionFunc, publishResponse publishFunc) { + // basic permissions check + resp := checkPermission(0) + if resp != nil { + publishResponse(resp, nostr.Tags{}) + return + } + + logger.Logger.WithFields(logrus.Fields{ + "request_event_id": requestEventId, + }).Info("Getting info") + + info, err := controller.lnClient.GetInfo(ctx) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "request_event_id": requestEventId, + }).Infof("Failed to fetch node info: %v", err) + + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: models.ERROR_INTERNAL, + Message: err.Error(), + }, + }, nostr.Tags{}) + return + } + + network := info.Network + // Some implementations return "bitcoin" while NIP47 expects "mainnet" + if network == "bitcoin" { + network = "mainnet" + } + + responsePayload := &getInfoResponse{ + Alias: info.Alias, + Color: info.Color, + Pubkey: info.Pubkey, + Network: network, + BlockHeight: info.BlockHeight, + BlockHash: info.BlockHash, + Methods: controller.permissionsService.GetPermittedMethods(app), + } + + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Result: responsePayload, + }, nostr.Tags{}) +} diff --git a/nip47/controllers/get_info_controller_test.go b/nip47/controllers/get_info_controller_test.go new file mode 100644 index 000000000..925a6ba13 --- /dev/null +++ b/nip47/controllers/get_info_controller_test.go @@ -0,0 +1,114 @@ +package controllers + +import ( + "context" + "encoding/json" + "testing" + + "github.com/nbd-wtf/go-nostr" + "github.com/stretchr/testify/assert" + + "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/getAlby/nostr-wallet-connect/nip47/permissions" + "github.com/getAlby/nostr-wallet-connect/tests" +) + +const nip47GetInfoJson = ` +{ + "method": "get_info" +} +` + +// TODO: info event should always return something +func TestHandleGetInfoEvent_NoPermission(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47GetInfoJson), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + checkPermission := func(amountMsat uint64) *models.Response { + return &models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: models.ERROR_RESTRICTED, + }, + } + } + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + + NewGetInfoController(permissionsSvc, svc.LNClient). + HandleGetInfoEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse) + + assert.Nil(t, publishedResponse.Result) + assert.Equal(t, models.ERROR_RESTRICTED, publishedResponse.Error.Code) +} + +func TestHandleGetInfoEvent_WithPermission(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47GetInfoJson), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + appPermission := &db.AppPermission{ + AppId: app.ID, + RequestMethod: models.GET_INFO_METHOD, + ExpiresAt: nil, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + checkPermission := func(amountMsat uint64) *models.Response { + return nil + } + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + + NewGetInfoController(permissionsSvc, svc.LNClient). + HandleGetInfoEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse) + + assert.Nil(t, publishedResponse.Error) + nodeInfo := publishedResponse.Result.(*getInfoResponse) + assert.Equal(t, tests.MockNodeInfo.Alias, nodeInfo.Alias) + assert.Equal(t, tests.MockNodeInfo.Color, nodeInfo.Color) + assert.Equal(t, tests.MockNodeInfo.Pubkey, nodeInfo.Pubkey) + assert.Equal(t, tests.MockNodeInfo.Network, nodeInfo.Network) + assert.Equal(t, tests.MockNodeInfo.BlockHeight, nodeInfo.BlockHeight) + assert.Equal(t, tests.MockNodeInfo.BlockHash, nodeInfo.BlockHash) + assert.Equal(t, []string{"get_info"}, nodeInfo.Methods) +} diff --git a/nip47/controllers/list_transactions_controller.go b/nip47/controllers/list_transactions_controller.go new file mode 100644 index 000000000..e5d7c261a --- /dev/null +++ b/nip47/controllers/list_transactions_controller.go @@ -0,0 +1,87 @@ +package controllers + +import ( + "context" + + "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/logger" + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/nbd-wtf/go-nostr" + "github.com/sirupsen/logrus" +) + +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 []models.Transaction `json:"transactions"` +} + +type listTransactionsController struct { + lnClient lnclient.LNClient +} + +func NewListTransactionsController(lnClient lnclient.LNClient) *listTransactionsController { + return &listTransactionsController{ + lnClient: lnClient, + } +} + +func (controller *listTransactionsController) HandleListTransactionsEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, checkPermission checkPermissionFunc, publishResponse publishFunc) { + // basic permissions check + resp := checkPermission(0) + if resp != nil { + publishResponse(resp, nostr.Tags{}) + return + } + + listParams := &listTransactionsParams{} + resp = decodeRequest(nip47Request, listParams) + if resp != nil { + publishResponse(resp, nostr.Tags{}) + return + } + + logger.Logger.WithFields(logrus.Fields{ + "params": listParams, + "request_event_id": requestEventId, + }).Info("Fetching transactions") + + limit := listParams.Limit + maxLimit := uint64(50) + if limit == 0 || limit > maxLimit { + // make sure a sensible limit is passed + limit = maxLimit + } + transactions, err := controller.lnClient.ListTransactions(ctx, listParams.From, listParams.Until, limit, listParams.Offset, listParams.Unpaid, listParams.Type) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "params": listParams, + "request_event_id": requestEventId, + }).WithError(err).Error("Failed to fetch transactions") + + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: models.ERROR_INTERNAL, + Message: err.Error(), + }, + }, nostr.Tags{}) + return + } + + responsePayload := &listTransactionsResponse{ + Transactions: transactions, + } + + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Result: responsePayload, + }, nostr.Tags{}) +} diff --git a/nip47/controllers/list_transactions_controller_test.go b/nip47/controllers/list_transactions_controller_test.go new file mode 100644 index 000000000..b838b73d5 --- /dev/null +++ b/nip47/controllers/list_transactions_controller_test.go @@ -0,0 +1,105 @@ +package controllers + +import ( + "context" + "encoding/json" + "testing" + + "github.com/nbd-wtf/go-nostr" + "github.com/stretchr/testify/assert" + + "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/getAlby/nostr-wallet-connect/tests" +) + +const nip47ListTransactionsJson = ` +{ + "method": "list_transactions", + "params": { + "from": 1693876973, + "until": 1694876973, + "limit": 10, + "offset": 0, + "type": "incoming" + } +} +` + +func TestHandleListTransactionsEvent_NoPermission(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47ListTransactionsJson), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + checkPermission := func(amountMsat uint64) *models.Response { + return &models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: models.ERROR_RESTRICTED, + }, + } + } + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + NewListTransactionsController(svc.LNClient). + HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, checkPermission, publishResponse) + + assert.Nil(t, publishedResponse.Result) + assert.Equal(t, models.ERROR_RESTRICTED, publishedResponse.Error.Code) +} + +func TestHandleListTransactionsEvent_WithPermission(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47ListTransactionsJson), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + checkPermission := func(amountMsat uint64) *models.Response { + return nil + } + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + NewListTransactionsController(svc.LNClient). + HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, checkPermission, publishResponse) + + assert.Nil(t, publishedResponse.Error) + + assert.Equal(t, 2, len(publishedResponse.Result.(*listTransactionsResponse).Transactions)) + transaction := publishedResponse.Result.(*listTransactionsResponse).Transactions[0] + assert.Equal(t, tests.MockTransactions[0].Type, transaction.Type) + assert.Equal(t, tests.MockTransactions[0].Invoice, transaction.Invoice) + assert.Equal(t, tests.MockTransactions[0].Description, transaction.Description) + assert.Equal(t, tests.MockTransactions[0].DescriptionHash, transaction.DescriptionHash) + assert.Equal(t, tests.MockTransactions[0].Preimage, transaction.Preimage) + assert.Equal(t, tests.MockTransactions[0].PaymentHash, transaction.PaymentHash) + assert.Equal(t, tests.MockTransactions[0].Amount, transaction.Amount) + assert.Equal(t, tests.MockTransactions[0].FeesPaid, transaction.FeesPaid) + assert.Equal(t, tests.MockTransactions[0].SettledAt, transaction.SettledAt) +} diff --git a/nip47/controllers/lookup_invoice_controller.go b/nip47/controllers/lookup_invoice_controller.go new file mode 100644 index 000000000..924d127f3 --- /dev/null +++ b/nip47/controllers/lookup_invoice_controller.go @@ -0,0 +1,104 @@ +package controllers + +import ( + "context" + "fmt" + "strings" + + "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/logger" + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/nbd-wtf/go-nostr" + decodepay "github.com/nbd-wtf/ln-decodepay" + "github.com/sirupsen/logrus" +) + +type lookupInvoiceParams struct { + Invoice string `json:"invoice"` + PaymentHash string `json:"payment_hash"` +} + +type lookupInvoiceResponse struct { + models.Transaction +} + +type lookupInvoiceController struct { + lnClient lnclient.LNClient +} + +func NewLookupInvoiceController(lnClient lnclient.LNClient) *lookupInvoiceController { + return &lookupInvoiceController{ + lnClient: lnClient, + } +} + +func (controller *lookupInvoiceController) HandleLookupInvoiceEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, checkPermission checkPermissionFunc, publishResponse publishFunc) { + // basic permissions check + resp := checkPermission(0) + if resp != nil { + publishResponse(resp, nostr.Tags{}) + return + } + + lookupInvoiceParams := &lookupInvoiceParams{} + resp = decodeRequest(nip47Request, lookupInvoiceParams) + if resp != nil { + publishResponse(resp, nostr.Tags{}) + return + } + + logger.Logger.WithFields(logrus.Fields{ + "invoice": lookupInvoiceParams.Invoice, + "payment_hash": lookupInvoiceParams.PaymentHash, + "request_event_id": requestEventId, + }).Info("Looking up invoice") + + paymentHash := lookupInvoiceParams.PaymentHash + + if paymentHash == "" { + paymentRequest, err := decodepay.Decodepay(strings.ToLower(lookupInvoiceParams.Invoice)) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "request_event_id": requestEventId, + "invoice": lookupInvoiceParams.Invoice, + }).WithError(err).Error("Failed to decode bolt11 invoice") + + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: models.ERROR_INTERNAL, + Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()), + }, + }, nostr.Tags{}) + return + } + paymentHash = paymentRequest.PaymentHash + } + + transaction, err := controller.lnClient.LookupInvoice(ctx, paymentHash) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "request_event_id": requestEventId, + "invoice": lookupInvoiceParams.Invoice, + "payment_hash": paymentHash, + }).Infof("Failed to lookup invoice: %v", err) + + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: models.ERROR_INTERNAL, + Message: err.Error(), + }, + }, nostr.Tags{}) + return + } + + responsePayload := &lookupInvoiceResponse{ + Transaction: *transaction, + } + + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Result: responsePayload, + }, nostr.Tags{}) +} diff --git a/nip47/controllers/lookup_invoice_controller_test.go b/nip47/controllers/lookup_invoice_controller_test.go new file mode 100644 index 000000000..2434b6c20 --- /dev/null +++ b/nip47/controllers/lookup_invoice_controller_test.go @@ -0,0 +1,99 @@ +package controllers + +import ( + "context" + "encoding/json" + "testing" + + "github.com/nbd-wtf/go-nostr" + "github.com/stretchr/testify/assert" + + "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/getAlby/nostr-wallet-connect/tests" +) + +const nip47LookupInvoiceJson = ` +{ + "method": "lookup_invoice", + "params": { + "payment_hash": "4ad9cd27989b514d868e755178378019903a8d78767e3fceb211af9dd00e7a94" + } +} +` + +func TestHandleLookupInvoiceEvent_NoPermission(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47LookupInvoiceJson), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + checkPermission := func(amountMsat uint64) *models.Response { + return &models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: models.ERROR_RESTRICTED, + }, + } + } + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + NewLookupInvoiceController(svc.LNClient). + HandleLookupInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, checkPermission, publishResponse) + + assert.Nil(t, publishedResponse.Result) + assert.Equal(t, models.ERROR_RESTRICTED, publishedResponse.Error.Code) +} + +func TestHandleLookupInvoiceEvent_WithPermission(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47LookupInvoiceJson), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + checkPermission := func(amountMsat uint64) *models.Response { + return nil + } + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + NewLookupInvoiceController(svc.LNClient). + HandleLookupInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, checkPermission, publishResponse) + + assert.Nil(t, publishedResponse.Error) + transaction := publishedResponse.Result.(*lookupInvoiceResponse) + assert.Equal(t, tests.MockTransaction.Type, transaction.Type) + assert.Equal(t, tests.MockTransaction.Invoice, transaction.Invoice) + assert.Equal(t, tests.MockTransaction.Description, transaction.Description) + assert.Equal(t, tests.MockTransaction.DescriptionHash, transaction.DescriptionHash) + assert.Equal(t, tests.MockTransaction.Preimage, transaction.Preimage) + assert.Equal(t, tests.MockTransaction.PaymentHash, transaction.PaymentHash) + assert.Equal(t, tests.MockTransaction.Amount, transaction.Amount) + assert.Equal(t, tests.MockTransaction.FeesPaid, transaction.FeesPaid) + assert.Equal(t, tests.MockTransaction.SettledAt, transaction.SettledAt) +} diff --git a/nip47/controllers/make_invoice_controller.go b/nip47/controllers/make_invoice_controller.go new file mode 100644 index 000000000..fad18e260 --- /dev/null +++ b/nip47/controllers/make_invoice_controller.go @@ -0,0 +1,89 @@ +package controllers + +import ( + "context" + + "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/logger" + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/nbd-wtf/go-nostr" + "github.com/sirupsen/logrus" +) + +type makeInvoiceParams struct { + Amount int64 `json:"amount"` + Description string `json:"description"` + DescriptionHash string `json:"description_hash"` + Expiry int64 `json:"expiry"` +} +type makeInvoiceResponse struct { + models.Transaction +} + +type makeInvoiceController struct { + lnClient lnclient.LNClient +} + +func NewMakeInvoiceController(lnClient lnclient.LNClient) *makeInvoiceController { + return &makeInvoiceController{ + lnClient: lnClient, + } +} + +func (controller *makeInvoiceController) HandleMakeInvoiceEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, checkPermission checkPermissionFunc, publishResponse publishFunc) { + // basic permissions check + resp := checkPermission(0) + if resp != nil { + publishResponse(resp, nostr.Tags{}) + return + } + + makeInvoiceParams := &makeInvoiceParams{} + resp = decodeRequest(nip47Request, makeInvoiceParams) + if resp != nil { + publishResponse(resp, nostr.Tags{}) + return + } + + logger.Logger.WithFields(logrus.Fields{ + "request_event_id": requestEventId, + "amount": makeInvoiceParams.Amount, + "description": makeInvoiceParams.Description, + "description_hash": makeInvoiceParams.DescriptionHash, + "expiry": makeInvoiceParams.Expiry, + }).Info("Making invoice") + + expiry := makeInvoiceParams.Expiry + if expiry == 0 { + expiry = 86400 + } + + transaction, err := controller.lnClient.MakeInvoice(ctx, makeInvoiceParams.Amount, makeInvoiceParams.Description, makeInvoiceParams.DescriptionHash, expiry) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "request_event_id": requestEventId, + "amount": makeInvoiceParams.Amount, + "description": makeInvoiceParams.Description, + "descriptionHash": makeInvoiceParams.DescriptionHash, + "expiry": makeInvoiceParams.Expiry, + }).Infof("Failed to make invoice: %v", err) + + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: models.ERROR_INTERNAL, + Message: err.Error(), + }, + }, nostr.Tags{}) + return + } + + responsePayload := &makeInvoiceResponse{ + Transaction: *transaction, + } + + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Result: responsePayload, + }, nostr.Tags{}) +} diff --git a/nip47/controllers/make_invoice_controller_test.go b/nip47/controllers/make_invoice_controller_test.go new file mode 100644 index 000000000..fc7fb436a --- /dev/null +++ b/nip47/controllers/make_invoice_controller_test.go @@ -0,0 +1,92 @@ +package controllers + +import ( + "context" + "encoding/json" + "testing" + + "github.com/nbd-wtf/go-nostr" + "github.com/stretchr/testify/assert" + + "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/getAlby/nostr-wallet-connect/tests" +) + +const nip47MakeInvoiceJson = ` +{ + "method": "make_invoice", + "params": { + "amount": 1000, + "description": "[[\"text/identifier\",\"hello@getalby.com\"],[\"text/plain\",\"Sats for Alby\"]]", + "expiry": 3600 + } +} +` + +func TestHandleMakeInvoiceEvent_NoPermission(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47MakeInvoiceJson), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + checkPermission := func(amountMsat uint64) *models.Response { + return &models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: models.ERROR_RESTRICTED, + }, + } + } + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + NewMakeInvoiceController(svc.LNClient). + HandleMakeInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, checkPermission, publishResponse) + + assert.Nil(t, publishedResponse.Result) + assert.Equal(t, models.ERROR_RESTRICTED, publishedResponse.Error.Code) +} + +func TestHandleMakeInvoiceEvent_WithPermission(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47MakeInvoiceJson), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + checkPermission := func(amountMsat uint64) *models.Response { + return nil + } + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + NewMakeInvoiceController(svc.LNClient). + HandleMakeInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, checkPermission, publishResponse) + + assert.Nil(t, publishedResponse.Error) + assert.Equal(t, tests.MockTransaction.Invoice, publishedResponse.Result.(*makeInvoiceResponse).Invoice) +} diff --git a/nip47/controllers/models.go b/nip47/controllers/models.go new file mode 100644 index 000000000..7cc6ef604 --- /dev/null +++ b/nip47/controllers/models.go @@ -0,0 +1,14 @@ +package controllers + +import ( + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/nbd-wtf/go-nostr" +) + +type checkPermissionFunc = func(amountMsat uint64) *models.Response +type publishFunc = func(*models.Response, nostr.Tags) + +type payResponse struct { + Preimage string `json:"preimage"` + FeesPaid *uint64 `json:"fees_paid"` +} diff --git a/nip47/controllers/multi_pay_invoice_controller.go b/nip47/controllers/multi_pay_invoice_controller.go new file mode 100644 index 000000000..b0a29db5b --- /dev/null +++ b/nip47/controllers/multi_pay_invoice_controller.go @@ -0,0 +1,91 @@ +package controllers + +import ( + "context" + "fmt" + "strings" + "sync" + + "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/logger" + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/nbd-wtf/go-nostr" + decodepay "github.com/nbd-wtf/ln-decodepay" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type multiPayInvoiceElement struct { + payInvoiceParams + Id string `json:"id"` +} + +type multiPayInvoiceParams struct { + Invoices []multiPayInvoiceElement `json:"invoices"` +} + +type multiMultiPayInvoiceController struct { + lnClient lnclient.LNClient + db *gorm.DB + eventPublisher events.EventPublisher +} + +func NewMultiPayInvoiceController(lnClient lnclient.LNClient, db *gorm.DB, eventPublisher events.EventPublisher) *multiMultiPayInvoiceController { + return &multiMultiPayInvoiceController{ + lnClient: lnClient, + db: db, + eventPublisher: eventPublisher, + } +} + +func (controller *multiMultiPayInvoiceController) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, app *db.App, checkPermission checkPermissionFunc, publishResponse publishFunc) { + multiPayParams := &multiPayInvoiceParams{} + resp := decodeRequest(nip47Request, multiPayParams) + if resp != nil { + publishResponse(resp, nostr.Tags{}) + return + } + + var wg sync.WaitGroup + for _, invoiceInfo := range multiPayParams.Invoices { + wg.Add(1) + go func(invoiceInfo multiPayInvoiceElement) { + defer wg.Done() + bolt11 := invoiceInfo.Invoice + // Convert invoice to lowercase string + bolt11 = strings.ToLower(bolt11) + paymentRequest, err := decodepay.Decodepay(bolt11) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "request_event_id": requestEventId, + "appId": app.ID, + "bolt11": bolt11, + }).Errorf("Failed to decode bolt11 invoice: %v", err) + + // TODO: Decide what to do if id is empty + dTag := []string{"d", invoiceInfo.Id} + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: models.ERROR_INTERNAL, + Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()), + }, + }, nostr.Tags{dTag}) + return + } + + invoiceDTagValue := invoiceInfo.Id + if invoiceDTagValue == "" { + invoiceDTagValue = paymentRequest.PaymentHash + } + dTag := []string{"d", invoiceDTagValue} + + NewPayInvoiceController(controller.lnClient, controller.db, controller.eventPublisher). + pay(ctx, bolt11, &paymentRequest, nip47Request, requestEventId, app, checkPermission, publishResponse, nostr.Tags{dTag}) + }(invoiceInfo) + } + + wg.Wait() +} diff --git a/nip47/controllers/multi_pay_invoice_controller_test.go b/nip47/controllers/multi_pay_invoice_controller_test.go new file mode 100644 index 000000000..3e5c2a0a7 --- /dev/null +++ b/nip47/controllers/multi_pay_invoice_controller_test.go @@ -0,0 +1,212 @@ +package controllers + +import ( + "context" + "encoding/json" + "testing" + + "github.com/nbd-wtf/go-nostr" + "github.com/stretchr/testify/assert" + + "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/getAlby/nostr-wallet-connect/tests" +) + +const nip47MultiPayJson = ` +{ + "method": "multi_pay_invoice", + "params": { + "invoices": [{ + "invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" + }, + { + "invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" + } + ] + } +} +` + +const nip47MultiPayOneOverflowingBudgetJson = ` +{ + "method": "multi_pay_invoice", + "params": { + "invoices": [{ + "invoice": "lnbcrt5u1pjuywzppp5h69dt59cypca2wxu69sw8ga0g39a3yx7dqug5nthrw3rcqgfdu4qdqqcqzzsxqyz5vqsp5gzlpzszyj2k30qmpme7jsfzr24wqlvt9xdmr7ay34lfelz050krs9qyyssq038x07nh8yuv8hdpjh5y8kqp7zcd62ql9na9xh7pla44htjyy02sz23q7qm2tza6ct4ypljk54w9k9qsrsu95usk8ce726ytep6vhhsq9mhf9a" + }, + { + "invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" + } + ] + } +} +` + +const nip47MultiPayOneMalformedInvoiceJson = ` +{ + "method": "multi_pay_invoice", + "params": { + "invoices": [{ + "invoice": "", + "id": "invoiceId123" + }, + { + "invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" + } + ] + } +} +` + +func TestHandleMultiPayInvoiceEvent_NoPermission(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47MultiPayJson), nip47Request) + assert.NoError(t, err) + + responses := []*models.Response{} + dTags := []nostr.Tags{} + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + checkPermission := func(amountMsat uint64) *models.Response { + return &models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: models.ERROR_RESTRICTED, + }, + } + } + + publishResponse := func(response *models.Response, tags nostr.Tags) { + responses = append(responses, response) + dTags = append(dTags, tags) + } + + NewMultiPayInvoiceController(svc.LNClient, svc.DB, svc.EventPublisher). + HandleMultiPayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse) + + assert.Equal(t, 2, len(responses)) + assert.Equal(t, 2, len(dTags)) + for i := 0; i < len(responses); i++ { + assert.Equal(t, models.ERROR_RESTRICTED, responses[i].Error.Code) + assert.Equal(t, tests.MockPaymentHash, dTags[i].GetFirst([]string{"d"}).Value()) + assert.Equal(t, responses[i].Result, nil) + } +} + +func TestHandleMultiPayInvoiceEvent_WithPermission(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47MultiPayJson), nip47Request) + assert.NoError(t, err) + + responses := []*models.Response{} + dTags := []nostr.Tags{} + + publishResponse := func(response *models.Response, tags nostr.Tags) { + responses = append(responses, response) + dTags = append(dTags, tags) + } + + checkPermission := func(amountMsat uint64) *models.Response { + return nil + } + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + NewMultiPayInvoiceController(svc.LNClient, svc.DB, svc.EventPublisher). + HandleMultiPayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse) + + assert.Equal(t, 2, len(responses)) + for i := 0; i < len(responses); i++ { + assert.Equal(t, "123preimage", responses[i].Result.(payResponse).Preimage) + assert.Equal(t, tests.MockPaymentHash, dTags[i].GetFirst([]string{"d"}).Value()) + assert.Nil(t, responses[i].Error) + } + +} + +func TestHandleMultiPayInvoiceEvent_OneMalformedInvoice(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47MultiPayOneMalformedInvoiceJson), nip47Request) + assert.NoError(t, err) + + responses := []*models.Response{} + dTags := []nostr.Tags{} + + publishResponse := func(response *models.Response, tags nostr.Tags) { + responses = append(responses, response) + dTags = append(dTags, tags) + } + + checkPermission := func(amountMsat uint64) *models.Response { + return nil + } + requestEvent := &db.RequestEvent{} + svc.DB.Save(requestEvent) + + NewMultiPayInvoiceController(svc.LNClient, svc.DB, svc.EventPublisher). + HandleMultiPayInvoiceEvent(ctx, nip47Request, requestEvent.ID, app, checkPermission, publishResponse) + + assert.Equal(t, 2, len(responses)) + assert.Equal(t, "invoiceId123", dTags[0].GetFirst([]string{"d"}).Value()) + assert.Equal(t, models.ERROR_INTERNAL, responses[0].Error.Code) + assert.Nil(t, responses[0].Result) + + assert.Equal(t, tests.MockPaymentHash, dTags[1].GetFirst([]string{"d"}).Value()) + assert.Equal(t, "123preimage", responses[1].Result.(payResponse).Preimage) + assert.Nil(t, responses[1].Error) + +} + +// TODO: fix and re-enable this as a separate test +// // budget overflow +// newMaxAmount := 500 +// 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), nip47Request) +// assert.NoError(t, err) + +// responses = []*models.Response{} +// dTags = []nostr.Tags{} +// NewMultiPayInvoiceController(svc.LNClient, svc.DB, svc.EventPublisher). +// HandleMultiPayInvoiceEvent(ctx, nip47Request, requestEvent.ID, app, checkPermission, publishResponse) + +// // might be flaky because the two requests run concurrently +// // and there's more chance that the failed respons calls the +// // publishResponse as it's called earlier +// assert.Equal(t, responses[0].Error.Code, models.ERROR_QUOTA_EXCEEDED) +// assert.Equal(t, tests.MockPaymentHash500, dTags[0].GetFirst([]string{"d"}).Value()) +// assert.Equal(t, responses[1].Result.(payResponse).Preimage, "123preimage") +// assert.Equal(t, tests.MockPaymentHash, dTags[1].GetFirst([]string{"d"}).Value()) diff --git a/nip47/controllers/multi_pay_keysend_controller.go b/nip47/controllers/multi_pay_keysend_controller.go new file mode 100644 index 000000000..7fca0b558 --- /dev/null +++ b/nip47/controllers/multi_pay_keysend_controller.go @@ -0,0 +1,64 @@ +package controllers + +import ( + "context" + "sync" + + "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/nip47/models" + "github.com/nbd-wtf/go-nostr" + "gorm.io/gorm" +) + +type multiPayKeysendParams struct { + Keysends []multiPayKeysendElement `json:"keysends"` +} + +type multiPayKeysendElement struct { + payKeysendParams + Id string `json:"id"` +} + +type multiMultiPayKeysendController struct { + lnClient lnclient.LNClient + db *gorm.DB + eventPublisher events.EventPublisher +} + +func NewMultiPayKeysendController(lnClient lnclient.LNClient, db *gorm.DB, eventPublisher events.EventPublisher) *multiMultiPayKeysendController { + return &multiMultiPayKeysendController{ + lnClient: lnClient, + db: db, + eventPublisher: eventPublisher, + } +} + +func (controller *multiMultiPayKeysendController) HandleMultiPayKeysendEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, app *db.App, checkPermission checkPermissionFunc, publishResponse publishFunc) { + multiPayParams := &multiPayKeysendParams{} + resp := decodeRequest(nip47Request, multiPayParams) + if resp != nil { + publishResponse(resp, nostr.Tags{}) + return + } + + var wg sync.WaitGroup + for _, keysendInfo := range multiPayParams.Keysends { + wg.Add(1) + go func(keysendInfo multiPayKeysendElement) { + defer wg.Done() + + keysendDTagValue := keysendInfo.Id + if keysendDTagValue == "" { + keysendDTagValue = keysendInfo.Pubkey + } + dTag := []string{"d", keysendDTagValue} + + NewPayKeysendController(controller.lnClient, controller.db, controller.eventPublisher). + pay(ctx, &keysendInfo.payKeysendParams, nip47Request, requestEventId, app, checkPermission, publishResponse, nostr.Tags{dTag}) + }(keysendInfo) + } + + wg.Wait() +} diff --git a/nip47/controllers/multi_pay_keysend_controller_test.go b/nip47/controllers/multi_pay_keysend_controller_test.go new file mode 100644 index 000000000..b74ecc0b7 --- /dev/null +++ b/nip47/controllers/multi_pay_keysend_controller_test.go @@ -0,0 +1,174 @@ +package controllers + +import ( + "context" + "encoding/json" + "testing" + + "github.com/nbd-wtf/go-nostr" + "github.com/stretchr/testify/assert" + + "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/getAlby/nostr-wallet-connect/tests" +) + +const nip47MultiPayKeysendJson = ` +{ + "method": "multi_pay_keysend", + "params": { + "keysends": [{ + "amount": 123000, + "pubkey": "123pubkey", + "tlv_records": [{ + "type": 5482373484, + "value": "fajsn341414fq" + }] + }, + { + "amount": 123000, + "pubkey": "123pubkey", + "tlv_records": [{ + "type": 5482373484, + "value": "fajsn341414fq" + }] + } + ] + } +} +` + +const nip47MultiPayKeysendOneOverflowingBudgetJson = ` +{ + "method": "multi_pay_keysend", + "params": { + "keysends": [{ + "amount": 123000, + "pubkey": "123pubkey", + "id": "customId", + "tlv_records": [{ + "type": 5482373484, + "value": "fajsn341414fq" + }] + }, + { + "amount": 500000, + "pubkey": "500pubkey", + "tlv_records": [{ + "type": 5482373484, + "value": "fajsn341414fq" + }] + } + ] + } +} +` + +func TestHandleMultiPayKeysendEvent_NoPermission(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47MultiPayKeysendJson), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + responses := []*models.Response{} + dTags := []nostr.Tags{} + + checkPermission := func(amountMsat uint64) *models.Response { + return &models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: models.ERROR_RESTRICTED, + }, + } + } + + publishResponse := func(response *models.Response, tags nostr.Tags) { + responses = append(responses, response) + dTags = append(dTags, tags) + } + + NewMultiPayKeysendController(svc.LNClient, svc.DB, svc.EventPublisher). + HandleMultiPayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse) + + assert.Equal(t, 2, len(responses)) + for i := 0; i < len(responses); i++ { + assert.Equal(t, models.ERROR_RESTRICTED, responses[i].Error.Code) + assert.Nil(t, responses[i].Result) + } + +} + +func TestHandleMultiPayKeysendEvent_WithPermission(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47MultiPayKeysendJson), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + responses := []*models.Response{} + dTags := []nostr.Tags{} + + checkPermission := func(amountMsat uint64) *models.Response { + return nil + } + + publishResponse := func(response *models.Response, tags nostr.Tags) { + responses = append(responses, response) + dTags = append(dTags, tags) + } + + NewMultiPayKeysendController(svc.LNClient, svc.DB, svc.EventPublisher). + HandleMultiPayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse) + + assert.Equal(t, 2, len(responses)) + for i := 0; i < len(responses); i++ { + assert.Equal(t, "12345preimage", responses[i].Result.(payResponse).Preimage) + assert.Nil(t, responses[i].Error) + assert.Equal(t, "123pubkey", dTags[i].GetFirst([]string{"d"}).Value()) + } +} + +// TODO: fix and re-enable this as a separate test +// budget overflow +/*newMaxAmount := 500 +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) +assert.NoError(t, err) + +payload, err = nip04.Encrypt(nip47MultiPayKeysendOneOverflowingBudgetJson, ss) +assert.NoError(t, err) +reqEvent.Content = payload + +reqEvent.ID = "multi_pay_keysend_with_budget_overflow" +requestEvent.NostrId = reqEvent.ID +responses = []*models.Response{} +dTags = []nostr.Tags{} +svc.nip47Svc.HandleMultiPayKeysendEvent(ctx, request, requestEvent, app, publishResponse) + +assert.Equal(t, responses[0].Error.Code, models.ERROR_QUOTA_EXCEEDED) +assert.Equal(t, "500pubkey", dTags[0].GetFirst([]string{"d"}).Value()) +assert.Equal(t, responses[1].Result.(payResponse).Preimage, "12345preimage") +assert.Equal(t, "customId", dTags[1].GetFirst([]string{"d"}).Value())*/ diff --git a/nip47/controllers/pay_invoice_controller.go b/nip47/controllers/pay_invoice_controller.go new file mode 100644 index 000000000..1d3aa0c31 --- /dev/null +++ b/nip47/controllers/pay_invoice_controller.go @@ -0,0 +1,138 @@ +package controllers + +import ( + "context" + "fmt" + "strings" + + "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/logger" + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/nbd-wtf/go-nostr" + decodepay "github.com/nbd-wtf/ln-decodepay" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type payInvoiceParams struct { + Invoice string `json:"invoice"` +} + +type payInvoiceController struct { + lnClient lnclient.LNClient + db *gorm.DB + eventPublisher events.EventPublisher +} + +func NewPayInvoiceController(lnClient lnclient.LNClient, db *gorm.DB, eventPublisher events.EventPublisher) *payInvoiceController { + return &payInvoiceController{ + lnClient: lnClient, + db: db, + eventPublisher: eventPublisher, + } +} + +func (controller *payInvoiceController) HandlePayInvoiceEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, app *db.App, checkPermission checkPermissionFunc, publishResponse publishFunc, tags nostr.Tags) { + payParams := &payInvoiceParams{} + resp := decodeRequest(nip47Request, payParams) + if resp != nil { + publishResponse(resp, tags) + return + } + + bolt11 := payParams.Invoice + // Convert invoice to lowercase string + bolt11 = strings.ToLower(bolt11) + paymentRequest, err := decodepay.Decodepay(bolt11) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "request_event_id": requestEventId, + "app_id": app.ID, + "bolt11": bolt11, + }).WithError(err).Error("Failed to decode bolt11 invoice") + + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: models.ERROR_INTERNAL, + Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()), + }, + }, tags) + return + } + + controller.pay(ctx, bolt11, &paymentRequest, nip47Request, requestEventId, app, checkPermission, publishResponse, tags) +} + +func (controller *payInvoiceController) pay(ctx context.Context, bolt11 string, paymentRequest *decodepay.Bolt11, nip47Request *models.Request, requestEventId uint, app *db.App, checkPermission checkPermissionFunc, publishResponse publishFunc, tags nostr.Tags) { + resp := checkPermission(uint64(paymentRequest.MSatoshi)) + if resp != nil { + publishResponse(resp, tags) + return + } + + payment := db.Payment{App: *app, RequestEventId: requestEventId, PaymentRequest: bolt11, Amount: uint(paymentRequest.MSatoshi / 1000)} + err := controller.db.Create(&payment).Error + if err != nil { + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: models.ERROR_INTERNAL, + Message: err.Error(), + }, + }, tags) + return + } + + logger.Logger.WithFields(logrus.Fields{ + "request_event_id": requestEventId, + "app_id": app.ID, + "bolt11": bolt11, + }).Info("Sending payment") + + response, err := controller.lnClient.SendPaymentSync(ctx, bolt11) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "request_event_id": requestEventId, + "app_id": app.ID, + "bolt11": bolt11, + }).Infof("Failed to send payment: %v", err) + controller.eventPublisher.Publish(&events.Event{ + Event: "nwc_payment_failed", + Properties: map[string]interface{}{ + "error": err.Error(), + "invoice": bolt11, + "amount": paymentRequest.MSatoshi / 1000, + }, + }) + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: models.ERROR_INTERNAL, + Message: err.Error(), + }, + }, tags) + return + } + payment.Preimage = &response.Preimage + // TODO: save payment fee + controller.db.Save(&payment) + + controller.eventPublisher.Publish(&events.Event{ + Event: "nwc_payment_succeeded", + Properties: map[string]interface{}{ + "bolt11": bolt11, + "amount": paymentRequest.MSatoshi / 1000, + }, + }) + + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Result: payResponse{ + Preimage: response.Preimage, + FeesPaid: response.Fee, + }, + }, tags) +} diff --git a/nip47/controllers/pay_invoice_controller_test.go b/nip47/controllers/pay_invoice_controller_test.go new file mode 100644 index 000000000..92b17502b --- /dev/null +++ b/nip47/controllers/pay_invoice_controller_test.go @@ -0,0 +1,138 @@ +package controllers + +import ( + "context" + "encoding/json" + "testing" + + "github.com/nbd-wtf/go-nostr" + "github.com/stretchr/testify/assert" + + "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/getAlby/nostr-wallet-connect/tests" +) + +const nip47PayInvoiceJson = ` +{ + "method": "pay_invoice", + "params": { + "invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" + } +} +` + +const nip47PayJsonNoInvoice = ` +{ + "method": "pay_invoice", + "params": { + "something": "else" + } +} +` + +func TestHandlePayInvoiceEvent_NoPermission(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47PayInvoiceJson), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + checkPermission := func(amountMsat uint64) *models.Response { + return &models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: models.ERROR_RESTRICTED, + }, + } + } + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + NewPayInvoiceController(svc.LNClient, svc.DB, svc.EventPublisher). + HandlePayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse, nostr.Tags{}) + + assert.Equal(t, models.ERROR_RESTRICTED, publishedResponse.Error.Code) + +} + +func TestHandlePayInvoiceEvent_WithPermission(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47PayInvoiceJson), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + checkPermission := func(amountMsat uint64) *models.Response { + return nil + } + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + NewPayInvoiceController(svc.LNClient, svc.DB, svc.EventPublisher). + HandlePayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse, nostr.Tags{}) + + assert.Equal(t, "123preimage", publishedResponse.Result.(payResponse).Preimage) +} + +func TestHandlePayInvoiceEvent_MalformedInvoice(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47PayJsonNoInvoice), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + checkPermission := func(amountMsat uint64) *models.Response { + return nil + } + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + NewPayInvoiceController(svc.LNClient, svc.DB, svc.EventPublisher). + HandlePayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse, nostr.Tags{}) + + assert.Nil(t, publishedResponse.Result) + assert.Equal(t, models.ERROR_INTERNAL, publishedResponse.Error.Code) +} diff --git a/nip47/controllers/pay_keysend_controller.go b/nip47/controllers/pay_keysend_controller.go new file mode 100644 index 000000000..734f37946 --- /dev/null +++ b/nip47/controllers/pay_keysend_controller.go @@ -0,0 +1,112 @@ +package controllers + +import ( + "context" + + "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/logger" + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/nbd-wtf/go-nostr" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type payKeysendParams struct { + Amount uint64 `json:"amount"` + Pubkey string `json:"pubkey"` + Preimage string `json:"preimage"` + TLVRecords []lnclient.TLVRecord `json:"tlv_records"` +} + +type payKeysendController struct { + lnClient lnclient.LNClient + db *gorm.DB + eventPublisher events.EventPublisher +} + +func NewPayKeysendController(lnClient lnclient.LNClient, db *gorm.DB, eventPublisher events.EventPublisher) *payKeysendController { + return &payKeysendController{ + lnClient: lnClient, + db: db, + eventPublisher: eventPublisher, + } +} + +func (controller *payKeysendController) HandlePayKeysendEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, app *db.App, checkPermission checkPermissionFunc, publishResponse publishFunc, tags nostr.Tags) { + payKeysendParams := &payKeysendParams{} + resp := decodeRequest(nip47Request, payKeysendParams) + if resp != nil { + publishResponse(resp, tags) + return + } + controller.pay(ctx, payKeysendParams, nip47Request, requestEventId, app, checkPermission, publishResponse, tags) +} + +func (controller *payKeysendController) pay(ctx context.Context, payKeysendParams *payKeysendParams, nip47Request *models.Request, requestEventId uint, app *db.App, checkPermission checkPermissionFunc, publishResponse publishFunc, tags nostr.Tags) { + resp := checkPermission(payKeysendParams.Amount) + if resp != nil { + publishResponse(resp, tags) + return + } + + payment := db.Payment{App: *app, RequestEventId: requestEventId, Amount: uint(payKeysendParams.Amount / 1000)} + err := controller.db.Create(&payment).Error + if err != nil { + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: models.ERROR_INTERNAL, + Message: err.Error(), + }, + }, tags) + return + } + + logger.Logger.WithFields(logrus.Fields{ + "request_event_id": requestEventId, + "appId": app.ID, + "senderPubkey": payKeysendParams.Pubkey, + }).Info("Sending keysend payment") + + preimage, err := controller.lnClient.SendKeysend(ctx, payKeysendParams.Amount, payKeysendParams.Pubkey, payKeysendParams.Preimage, payKeysendParams.TLVRecords) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "request_event_id": requestEventId, + "appId": app.ID, + "recipientPubkey": payKeysendParams.Pubkey, + }).Infof("Failed to send keysend payment: %v", err) + controller.eventPublisher.Publish(&events.Event{ + Event: "nwc_payment_failed", + Properties: map[string]interface{}{ + "error": err.Error(), + "keysend": true, + "amount": payKeysendParams.Amount / 1000, + }, + }) + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: models.ERROR_INTERNAL, + Message: err.Error(), + }, + }, tags) + return + } + payment.Preimage = &preimage + controller.db.Save(&payment) + controller.eventPublisher.Publish(&events.Event{ + Event: "nwc_payment_succeeded", + Properties: map[string]interface{}{ + "keysend": true, + "amount": payKeysendParams.Amount / 1000, + }, + }) + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Result: payResponse{ + Preimage: preimage, + }, + }, tags) +} diff --git a/nip47/controllers/pay_keysend_controller_test.go b/nip47/controllers/pay_keysend_controller_test.go new file mode 100644 index 000000000..d973e81a3 --- /dev/null +++ b/nip47/controllers/pay_keysend_controller_test.go @@ -0,0 +1,102 @@ +package controllers + +import ( + "context" + "encoding/json" + "testing" + + "github.com/nbd-wtf/go-nostr" + "github.com/stretchr/testify/assert" + + "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/getAlby/nostr-wallet-connect/tests" +) + +const nip47KeysendJson = ` +{ + "method": "pay_keysend", + "params": { + "amount": 123000, + "pubkey": "123pubkey", + "tlv_records": [{ + "type": 5482373484, + "value": "fajsn341414fq" + }] + } +} +` + +func TestHandlePayKeysendEvent_NoPermission(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47KeysendJson), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + checkPermission := func(amountMsat uint64) *models.Response { + return &models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: models.ERROR_RESTRICTED, + }, + } + } + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + NewPayKeysendController(svc.LNClient, svc.DB, svc.EventPublisher). + HandlePayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse, nostr.Tags{}) + + assert.Nil(t, publishedResponse.Result) + assert.Equal(t, models.ERROR_RESTRICTED, publishedResponse.Error.Code) +} + +func TestHandlePayKeysendEvent_WithPermission(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47KeysendJson), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + checkPermission := func(amountMsat uint64) *models.Response { + return nil + } + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + NewPayKeysendController(svc.LNClient, svc.DB, svc.EventPublisher). + HandlePayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse, nostr.Tags{}) + + assert.Nil(t, publishedResponse.Error) + assert.Equal(t, "12345preimage", publishedResponse.Result.(payResponse).Preimage) +} diff --git a/nip47/controllers/sign_message_controller.go b/nip47/controllers/sign_message_controller.go new file mode 100644 index 000000000..374ad633c --- /dev/null +++ b/nip47/controllers/sign_message_controller.go @@ -0,0 +1,75 @@ +package controllers + +import ( + "context" + + "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/logger" + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/nbd-wtf/go-nostr" + "github.com/sirupsen/logrus" +) + +type signMessageParams struct { + Message string `json:"message"` +} + +type signMessageResponse struct { + Message string `json:"message"` + Signature string `json:"signature"` +} + +type signMessageController struct { + lnClient lnclient.LNClient +} + +func NewSignMessageController(lnClient lnclient.LNClient) *signMessageController { + return &signMessageController{ + lnClient: lnClient, + } +} + +func (controller *signMessageController) HandleSignMessageEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, checkPermission checkPermissionFunc, publishResponse publishFunc) { + // basic permissions check + resp := checkPermission(0) + if resp != nil { + publishResponse(resp, nostr.Tags{}) + return + } + + signParams := &signMessageParams{} + resp = decodeRequest(nip47Request, signParams) + if resp != nil { + publishResponse(resp, nostr.Tags{}) + return + } + + logger.Logger.WithFields(logrus.Fields{ + "request_event_id": requestEventId, + }).Info("Signing message") + + signature, err := controller.lnClient.SignMessage(ctx, signParams.Message) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "request_event_id": requestEventId, + }).WithError(err).Error("Failed to sign message") + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: models.ERROR_INTERNAL, + Message: err.Error(), + }, + }, nostr.Tags{}) + return + } + + responsePayload := signMessageResponse{ + Message: signParams.Message, + Signature: signature, + } + + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Result: responsePayload, + }, nostr.Tags{}) +} diff --git a/nip47/event_handler.go b/nip47/event_handler.go new file mode 100644 index 000000000..405450111 --- /dev/null +++ b/nip47/event_handler.go @@ -0,0 +1,413 @@ +package nip47 + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "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/logger" + controllers "github.com/getAlby/nostr-wallet-connect/nip47/controllers" + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip04" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscription, event *nostr.Event, lnClient lnclient.LNClient) { + var nip47Response *models.Response + logger.Logger.WithFields(logrus.Fields{ + "requestEventNostrId": event.ID, + "eventKind": event.Kind, + }).Info("Processing Event") + + ss, err := nip04.ComputeSharedSecret(event.PubKey, svc.keys.GetNostrSecretKey()) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "requestEventNostrId": event.ID, + "eventKind": event.Kind, + }).WithError(err).Error("Failed to compute shared secret") + return + } + + // store request event + requestEvent := db.RequestEvent{AppId: nil, NostrId: event.ID, State: db.REQUEST_EVENT_STATE_HANDLER_EXECUTING} + err = svc.db.Create(&requestEvent).Error + if err != nil { + if errors.Is(err, gorm.ErrDuplicatedKey) { + logger.Logger.WithFields(logrus.Fields{ + "requestEventNostrId": event.ID, + }).Warn("Event already processed") + return + } + logger.Logger.WithFields(logrus.Fields{ + "requestEventNostrId": event.ID, + "eventKind": event.Kind, + }).WithError(err).Error("Failed to save nostr event") + nip47Response = &models.Response{ + Error: &models.Error{ + Code: models.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 { + logger.Logger.WithFields(logrus.Fields{ + "requestEventNostrId": event.ID, + "eventKind": event.Kind, + }).WithError(err).Error("Failed to process event") + } + svc.publishResponseEvent(ctx, sub, &requestEvent, resp, nil) + return + } + + app := db.App{} + err = svc.db.First(&app, &db.App{ + NostrPubkey: event.PubKey, + }).Error + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "nostrPubkey": event.PubKey, + }).WithError(err).Error("Failed to find app for nostr pubkey") + + nip47Response = &models.Response{ + Error: &models.Error{ + Code: models.ERROR_UNAUTHORIZED, + Message: "The public key does not have a wallet connected.", + }, + } + resp, err := svc.CreateResponse(event, nip47Response, nostr.Tags{}, ss) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "requestEventNostrId": event.ID, + "eventKind": event.Kind, + }).WithError(err).Error("Failed to process event") + } + svc.publishResponseEvent(ctx, sub, &requestEvent, resp, &app) + + requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR + err = svc.db.Save(&requestEvent).Error + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "nostrPubkey": event.PubKey, + }).WithError(err).Error("Failed to save state to nostr event") + } + return + } + + requestEvent.AppId = &app.ID + err = svc.db.Save(&requestEvent).Error + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "nostrPubkey": event.PubKey, + }).WithError(err).Error("Failed to save app to nostr event") + + nip47Response = &models.Response{ + Error: &models.Error{ + Code: models.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 { + logger.Logger.WithFields(logrus.Fields{ + "requestEventNostrId": event.ID, + "eventKind": event.Kind, + }).WithError(err).Error("Failed to process event") + } + svc.publishResponseEvent(ctx, sub, &requestEvent, resp, &app) + + requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR + err = svc.db.Save(&requestEvent).Error + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "nostrPubkey": event.PubKey, + }).WithError(err).Error("Failed to save state to nostr event") + } + + return + } + + logger.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.keys.GetNostrSecretKey()) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "requestEventNostrId": event.ID, + "eventKind": event.Kind, + }).WithError(err).Error("Failed to process event") + + requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR + err = svc.db.Save(&requestEvent).Error + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "nostrPubkey": event.PubKey, + }).WithError(err).Error("Failed to save state to nostr event") + } + + return + } + payload, err := nip04.Decrypt(event.Content, ss) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "requestEventNostrId": event.ID, + "eventKind": event.Kind, + "appId": app.ID, + }).WithError(err).Error("Failed to decrypt content") + logger.Logger.WithFields(logrus.Fields{ + "requestEventNostrId": event.ID, + "eventKind": event.Kind, + }).WithError(err).Error("Failed to process event") + + requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR + err = svc.db.Save(&requestEvent).Error + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "nostrPubkey": event.PubKey, + }).WithError(err).Error("Failed to save state to nostr event") + } + + return + } + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(payload), nip47Request) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "requestEventNostrId": event.ID, + "eventKind": event.Kind, + }).WithError(err).Error("Failed to process event") + + requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR + err = svc.db.Save(&requestEvent).Error + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "nostrPubkey": event.PubKey, + }).WithError(err).Error("Failed to save state to nostr event") + } + + return + } + + requestEvent.Method = nip47Request.Method + requestEvent.ContentData = payload + svc.db.Save(&requestEvent) // we ignore potential DB errors here as this only saves the method and content data + + // TODO: replace with a channel + // TODO: update all previous occurences of svc.publishResponseEvent to also use the channel + publishResponse := func(nip47Response *models.Response, tags nostr.Tags) { + resp, err := svc.CreateResponse(event, nip47Response, tags, ss) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "requestEventNostrId": event.ID, + "eventKind": event.Kind, + "appId": app.ID, + }).WithError(err).Error("Failed to create response") + requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR + } else { + err = svc.publishResponseEvent(ctx, sub, &requestEvent, resp, &app) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "requestEventNostrId": event.ID, + "eventKind": event.Kind, + "appId": app.ID, + }).WithError(err).Error("Failed to publish event") + requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR + } else { + requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_EXECUTED + logger.Logger.WithFields(logrus.Fields{ + "requestEventNostrId": event.ID, + "eventKind": event.Kind, + "appId": app.ID, + }).Info("Published response") + } + } + err = svc.db.Save(&requestEvent).Error + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "nostrPubkey": event.PubKey, + }).WithError(err).Error("Failed to save state to nostr event") + } + } + + checkPermission := func(amountMsat uint64) *models.Response { + hasPermission, code, message := svc.permissionsService.HasPermission(&app, nip47Request.Method, amountMsat) + if !hasPermission { + logger.Logger.WithFields(logrus.Fields{ + "request_event_id": requestEvent.ID, + "app_id": app.ID, + "code": code, + "message": message, + }).Error("App does not have permission") + + svc.eventPublisher.Publish(&events.Event{ + Event: "nwc_permission_denied", + Properties: map[string]interface{}{ + "request_method": nip47Request.Method, + "app_name": app.Name, + // "app_pubkey": app.NostrPubkey, + "code": code, + "message": message, + }, + }) + + return &models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: code, + Message: message, + }, + } + } + return nil + } + + logger.Logger.WithFields(logrus.Fields{ + "requestEventNostrId": event.ID, + "eventKind": event.Kind, + "appId": app.ID, + "method": nip47Request.Method, + "params": nip47Request.Params, + }).Info("Handling NIP-47 request") + + // TODO: controllers should share a common interface + switch nip47Request.Method { + case models.MULTI_PAY_INVOICE_METHOD: + controllers. + NewMultiPayInvoiceController(lnClient, svc.db, svc.eventPublisher). + HandleMultiPayInvoiceEvent(ctx, nip47Request, requestEvent.ID, &app, checkPermission, publishResponse) + case models.MULTI_PAY_KEYSEND_METHOD: + controllers. + NewMultiPayKeysendController(lnClient, svc.db, svc.eventPublisher). + HandleMultiPayKeysendEvent(ctx, nip47Request, requestEvent.ID, &app, checkPermission, publishResponse) + case models.PAY_INVOICE_METHOD: + controllers. + NewPayInvoiceController(lnClient, svc.db, svc.eventPublisher). + HandlePayInvoiceEvent(ctx, nip47Request, requestEvent.ID, &app, checkPermission, publishResponse, nostr.Tags{}) + case models.PAY_KEYSEND_METHOD: + controllers. + NewPayKeysendController(lnClient, svc.db, svc.eventPublisher). + HandlePayKeysendEvent(ctx, nip47Request, requestEvent.ID, &app, checkPermission, publishResponse, nostr.Tags{}) + case models.GET_BALANCE_METHOD: + controllers. + NewGetBalanceController(lnClient). + HandleGetBalanceEvent(ctx, nip47Request, requestEvent.ID, checkPermission, publishResponse) + case models.MAKE_INVOICE_METHOD: + controllers. + NewMakeInvoiceController(lnClient). + HandleMakeInvoiceEvent(ctx, nip47Request, requestEvent.ID, checkPermission, publishResponse) + case models.LOOKUP_INVOICE_METHOD: + controllers. + NewLookupInvoiceController(lnClient). + HandleLookupInvoiceEvent(ctx, nip47Request, requestEvent.ID, checkPermission, publishResponse) + case models.LIST_TRANSACTIONS_METHOD: + controllers. + NewListTransactionsController(lnClient). + HandleListTransactionsEvent(ctx, nip47Request, requestEvent.ID, checkPermission, publishResponse) + case models.GET_INFO_METHOD: + controllers. + NewGetInfoController(svc.permissionsService, lnClient). + HandleGetInfoEvent(ctx, nip47Request, requestEvent.ID, &app, checkPermission, publishResponse) + case models.SIGN_MESSAGE_METHOD: + controllers. + NewSignMessageController(lnClient). + HandleSignMessageEvent(ctx, nip47Request, requestEvent.ID, checkPermission, publishResponse) + default: + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: models.ERROR_NOT_IMPLEMENTED, + Message: fmt.Sprintf("Unknown method: %s", nip47Request.Method), + }, + }, nostr.Tags{}) + } +} + +func (svc *nip47Service) CreateResponse(initialEvent *nostr.Event, content interface{}, tags nostr.Tags, ss []byte) (result *nostr.Event, err error) { + payloadBytes, err := json.Marshal(content) + if err != nil { + return nil, err + } + msg, err := nip04.Encrypt(string(payloadBytes), ss) + if err != nil { + return nil, err + } + + allTags := nostr.Tags{[]string{"p", initialEvent.PubKey}, []string{"e", initialEvent.ID}} + allTags = append(allTags, tags...) + + resp := &nostr.Event{ + PubKey: svc.keys.GetNostrPublicKey(), + CreatedAt: nostr.Now(), + Kind: models.RESPONSE_KIND, + Tags: allTags, + Content: msg, + } + err = resp.Sign(svc.keys.GetNostrSecretKey()) + if err != nil { + return nil, err + } + return resp, nil +} + +func (svc *nip47Service) publishResponseEvent(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 := db.ResponseEvent{NostrId: resp.ID, RequestId: requestEvent.ID, State: "received"} + err := svc.db.Create(&responseEvent).Error + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "requestEventNostrId": requestEvent.NostrId, + "appId": appId, + "replyEventId": resp.ID, + }).WithError(err).Error("Failed to save response/reply event") + return err + } + + err = sub.Relay.Publish(ctx, *resp) + if err != nil { + responseEvent.State = db.RESPONSE_EVENT_STATE_PUBLISH_FAILED + logger.Logger.WithFields(logrus.Fields{ + "requestEventId": requestEvent.ID, + "requestNostrEventId": requestEvent.NostrId, + "appId": appId, + "responseEventId": responseEvent.ID, + "responseNostrEventId": resp.ID, + }).WithError(err).Error("Failed to publish reply") + } else { + responseEvent.State = db.RESPONSE_EVENT_STATE_PUBLISH_CONFIRMED + responseEvent.RepliedAt = time.Now() + logger.Logger.WithFields(logrus.Fields{ + "requestEventId": requestEvent.ID, + "requestNostrEventId": requestEvent.NostrId, + "appId": appId, + "responseEventId": responseEvent.ID, + "responseNostrEventId": resp.ID, + }).Info("Published reply") + } + + err = svc.db.Save(&responseEvent).Error + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "requestEventId": requestEvent.ID, + "requestNostrEventId": requestEvent.NostrId, + "appId": appId, + "responseEventId": responseEvent.ID, + "responseNostrEventId": resp.ID, + }).WithError(err).Error("Failed to update response/reply event") + return err + } + + return nil +} diff --git a/nip47/event_handler_test.go b/nip47/event_handler_test.go new file mode 100644 index 000000000..b1e874f69 --- /dev/null +++ b/nip47/event_handler_test.go @@ -0,0 +1,67 @@ +package nip47 + +import ( + "encoding/json" + "testing" + + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/getAlby/nostr-wallet-connect/tests" + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip04" + "github.com/stretchr/testify/assert" +) + +// TODO: test HandleEvent +// TODO: test a request cannot be processed twice +// TODO: test if an app doesn't exist it returns the right error code + +func TestCreateResponse(t *testing.T) { + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + reqPrivateKey := nostr.GeneratePrivateKey() + reqPubkey, err := nostr.GetPublicKey(reqPrivateKey) + assert.NoError(t, err) + + reqEvent := &nostr.Event{ + Kind: models.REQUEST_KIND, + PubKey: reqPubkey, + Content: "1", + } + + reqEvent.ID = "12345" + + ss, err := nip04.ComputeSharedSecret(reqPubkey, svc.Keys.GetNostrSecretKey()) + assert.NoError(t, err) + + type dummyResponse struct { + Foo int + } + + nip47Response := &models.Response{ + ResultType: "dummy_method", + Result: dummyResponse{ + Foo: 1000, + }, + } + + nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher) + + res, err := nip47svc.CreateResponse(reqEvent, nip47Response, nostr.Tags{}, ss) + 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.Keys.GetNostrPublicKey(), res.PubKey) + + decrypted, err := nip04.Decrypt(res.Content, ss) + assert.NoError(t, err) + unmarshalledResponse := models.Response{ + Result: &dummyResponse{}, + } + + err = json.Unmarshal([]byte(decrypted), &unmarshalledResponse) + assert.NoError(t, err) + assert.Equal(t, nip47Response.ResultType, unmarshalledResponse.ResultType) + assert.Equal(t, nip47Response.Result, *unmarshalledResponse.Result.(*dummyResponse)) +} diff --git a/nip47/models.go b/nip47/models.go deleted file mode 100644 index d2cabf98e..000000000 --- a/nip47/models.go +++ /dev/null @@ -1,173 +0,0 @@ -package nip47 - -import ( - "encoding/json" - - "github.com/getAlby/nostr-wallet-connect/lnclient" -) - -const ( - INFO_EVENT_KIND = 13194 - REQUEST_KIND = 23194 - RESPONSE_KIND = 23195 - NOTIFICATION_KIND = 23196 - PAY_INVOICE_METHOD = "pay_invoice" - GET_BALANCE_METHOD = "get_balance" - GET_INFO_METHOD = "get_info" - MAKE_INVOICE_METHOD = "make_invoice" - LOOKUP_INVOICE_METHOD = "lookup_invoice" - LIST_TRANSACTIONS_METHOD = "list_transactions" - PAY_KEYSEND_METHOD = "pay_keysend" - MULTI_PAY_INVOICE_METHOD = "multi_pay_invoice" - MULTI_PAY_KEYSEND_METHOD = "multi_pay_keysend" - SIGN_MESSAGE_METHOD = "sign_message" - ERROR_INTERNAL = "INTERNAL" - ERROR_NOT_IMPLEMENTED = "NOT_IMPLEMENTED" - ERROR_QUOTA_EXCEEDED = "QUOTA_EXCEEDED" - ERROR_INSUFFICIENT_BALANCE = "INSUFFICIENT_BALANCE" - ERROR_UNAUTHORIZED = "UNAUTHORIZED" - ERROR_EXPIRED = "EXPIRED" - ERROR_RESTRICTED = "RESTRICTED" - ERROR_BAD_REQUEST = "BAD_REQUEST" - OTHER = "OTHER" - CAPABILITIES = "pay_invoice pay_keysend get_balance get_info make_invoice lookup_invoice list_transactions multi_pay_invoice multi_pay_keysend sign_message notifications" - NOTIFICATION_TYPES = "payment_received" // same format as above e.g. "payment_received balance_updated payment_sent channel_opened channel_closed ..." -) - -// TODO: move other permissions here (e.g. all payment methods use pay_invoice) -const ( - NOTIFICATIONS_PERMISSION = "notifications" -) - -const ( - PAYMENT_RECEIVED_NOTIFICATION = "payment_received" -) - -const ( - BUDGET_RENEWAL_DAILY = "daily" - BUDGET_RENEWAL_WEEKLY = "weekly" - BUDGET_RENEWAL_MONTHLY = "monthly" - 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/models/models.go b/nip47/models/models.go new file mode 100644 index 000000000..1d8314d4a --- /dev/null +++ b/nip47/models/models.go @@ -0,0 +1,64 @@ +package models + +import ( + "encoding/json" + + "github.com/getAlby/nostr-wallet-connect/lnclient" +) + +const ( + INFO_EVENT_KIND = 13194 + REQUEST_KIND = 23194 + RESPONSE_KIND = 23195 + NOTIFICATION_KIND = 23196 + PAY_INVOICE_METHOD = "pay_invoice" + GET_BALANCE_METHOD = "get_balance" + GET_INFO_METHOD = "get_info" + MAKE_INVOICE_METHOD = "make_invoice" + LOOKUP_INVOICE_METHOD = "lookup_invoice" + LIST_TRANSACTIONS_METHOD = "list_transactions" + PAY_KEYSEND_METHOD = "pay_keysend" + MULTI_PAY_INVOICE_METHOD = "multi_pay_invoice" + MULTI_PAY_KEYSEND_METHOD = "multi_pay_keysend" + SIGN_MESSAGE_METHOD = "sign_message" + ERROR_INTERNAL = "INTERNAL" + ERROR_NOT_IMPLEMENTED = "NOT_IMPLEMENTED" + ERROR_QUOTA_EXCEEDED = "QUOTA_EXCEEDED" + ERROR_INSUFFICIENT_BALANCE = "INSUFFICIENT_BALANCE" + ERROR_UNAUTHORIZED = "UNAUTHORIZED" + ERROR_EXPIRED = "EXPIRED" + ERROR_RESTRICTED = "RESTRICTED" + ERROR_BAD_REQUEST = "BAD_REQUEST" + OTHER = "OTHER" + CAPABILITIES = "pay_invoice pay_keysend get_balance get_info make_invoice lookup_invoice list_transactions multi_pay_invoice multi_pay_keysend sign_message notifications" +) + +const ( + BUDGET_RENEWAL_DAILY = "daily" + BUDGET_RENEWAL_WEEKLY = "weekly" + BUDGET_RENEWAL_MONTHLY = "monthly" + 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 Error struct { + Code string `json:"code,omitempty"` + Message string `json:"message,omitempty"` +} diff --git a/nip47/nip47_service.go b/nip47/nip47_service.go new file mode 100644 index 000000000..06fa8fc99 --- /dev/null +++ b/nip47/nip47_service.go @@ -0,0 +1,58 @@ +package nip47 + +import ( + "context" + + "github.com/getAlby/nostr-wallet-connect/config" + "github.com/getAlby/nostr-wallet-connect/events" + "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/nip47/notifications" + permissions "github.com/getAlby/nostr-wallet-connect/nip47/permissions" + "github.com/getAlby/nostr-wallet-connect/service/keys" + "github.com/nbd-wtf/go-nostr" + "gorm.io/gorm" +) + +type nip47Service struct { + permissionsService permissions.PermissionsService + nip47NotificationQueue notifications.Nip47NotificationQueue + cfg config.Config + keys keys.Keys + db *gorm.DB + eventPublisher events.EventPublisher +} + +type Nip47Service interface { + StartNotifier(ctx context.Context, relay *nostr.Relay, lnClient lnclient.LNClient) + HandleEvent(ctx context.Context, sub *nostr.Subscription, event *nostr.Event, lnClient lnclient.LNClient) + PublishNip47Info(ctx context.Context, relay *nostr.Relay) error + CreateResponse(initialEvent *nostr.Event, content interface{}, tags nostr.Tags, ss []byte) (result *nostr.Event, err error) +} + +func NewNip47Service(db *gorm.DB, cfg config.Config, keys keys.Keys, eventPublisher events.EventPublisher) *nip47Service { + nip47NotificationQueue := notifications.NewNip47NotificationQueue() + eventPublisher.RegisterSubscriber(nip47NotificationQueue) + return &nip47Service{ + nip47NotificationQueue: nip47NotificationQueue, + cfg: cfg, + db: db, + permissionsService: permissions.NewPermissionsService(db, eventPublisher), + eventPublisher: eventPublisher, + keys: keys, + } +} + +func (svc *nip47Service) StartNotifier(ctx context.Context, relay *nostr.Relay, lnClient lnclient.LNClient) { + nip47Notifier := notifications.NewNip47Notifier(relay, svc.db, svc.cfg, svc.keys, svc.permissionsService, lnClient) + go func() { + for { + select { + case <-ctx.Done(): + // subscription ended + return + case event := <-svc.nip47NotificationQueue.Channel(): + nip47Notifier.ConsumeEvent(ctx, event) + } + } + }() +} diff --git a/nip47/notifications/models.go b/nip47/notifications/models.go new file mode 100644 index 000000000..e981dc603 --- /dev/null +++ b/nip47/notifications/models.go @@ -0,0 +1,17 @@ +package notifications + +import "github.com/getAlby/nostr-wallet-connect/nip47/models" + +type Notification struct { + Notification interface{} `json:"notification,omitempty"` + NotificationType string `json:"notification_type"` +} + +const ( + NOTIFICATION_TYPES = "payment_received" // e.g. "payment_received payment_sent balance_updated payment_sent channel_opened channel_closed ..." + PAYMENT_RECEIVED_NOTIFICATION = "payment_received" +) + +type PaymentReceivedNotification struct { + models.Transaction +} diff --git a/nip47/nip47_notification_queue.go b/nip47/notifications/nip47_notification_queue.go similarity index 74% rename from nip47/nip47_notification_queue.go rename to nip47/notifications/nip47_notification_queue.go index 41ce84f4d..59bf954b2 100644 --- a/nip47/nip47_notification_queue.go +++ b/nip47/notifications/nip47_notification_queue.go @@ -1,11 +1,11 @@ -package nip47 +package notifications import ( "context" "errors" "github.com/getAlby/nostr-wallet-connect/events" - "github.com/sirupsen/logrus" + "github.com/getAlby/nostr-wallet-connect/logger" ) type Nip47NotificationQueue interface { @@ -15,16 +15,14 @@ type Nip47NotificationQueue interface { type nip47NotificationQueue struct { channel chan *events.Event - logger *logrus.Logger } /* Queue events that will be consumed when the relay connection is online */ -func NewNip47NotificationQueue(logger *logrus.Logger) *nip47NotificationQueue { +func NewNip47NotificationQueue() *nip47NotificationQueue { return &nip47NotificationQueue{ channel: make(chan *events.Event, 1000), - logger: logger, } } @@ -33,7 +31,7 @@ func (q *nip47NotificationQueue) ConsumeEvent(ctx context.Context, event *events case q.channel <- event: // Put in the channel unless it is full return nil default: - q.logger.WithField("event", event).Error("NIP47NotificationQueue channel full. Discarding value") + logger.Logger.WithField("event", event).Error("NIP47NotificationQueue channel full. Discarding value") return errors.New("nip-47 notification queue full") } } diff --git a/nip47_notifier.go b/nip47/notifications/nip47_notifier.go similarity index 58% rename from nip47_notifier.go rename to nip47/notifications/nip47_notifier.go index 98ffc1d6e..da8eca929 100644 --- a/nip47_notifier.go +++ b/nip47/notifications/nip47_notifier.go @@ -1,17 +1,22 @@ -// TODO: move to nip47 -package main +package notifications import ( "context" "encoding/json" "errors" + "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/nip47" + "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/logger" + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/getAlby/nostr-wallet-connect/nip47/permissions" + "github.com/getAlby/nostr-wallet-connect/service/keys" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip04" "github.com/sirupsen/logrus" + "gorm.io/gorm" ) type Relay interface { @@ -19,14 +24,22 @@ type Relay interface { } type Nip47Notifier struct { - svc *Service - relay Relay + relay Relay + cfg config.Config + keys keys.Keys + lnClient lnclient.LNClient + db *gorm.DB + permissionsSvc permissions.PermissionsService } -func NewNip47Notifier(svc *Service, relay Relay) *Nip47Notifier { +func NewNip47Notifier(relay Relay, db *gorm.DB, cfg config.Config, keys keys.Keys, permissionsSvc permissions.PermissionsService, lnClient lnclient.LNClient) *Nip47Notifier { return &Nip47Notifier{ - svc: svc, - relay: relay, + relay: relay, + cfg: cfg, + db: db, + lnClient: lnClient, + permissionsSvc: permissionsSvc, + keys: keys, } } @@ -35,40 +48,36 @@ func (notifier *Nip47Notifier) ConsumeEvent(ctx context.Context, event *events.E return nil } - if notifier.svc.lnClient == nil { - return nil - } - paymentReceivedEventProperties, ok := event.Properties.(*events.PaymentReceivedEventProperties) if !ok { - notifier.svc.logger.WithField("event", event).Error("Failed to cast event") + logger.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) + transaction, err := notifier.lnClient.LookupInvoice(ctx, paymentReceivedEventProperties.PaymentHash) if err != nil { - notifier.svc.logger. + logger.Logger. WithField("paymentHash", paymentReceivedEventProperties.PaymentHash). WithError(err). Error("Failed to lookup invoice by payment hash") return err } - notifier.notifySubscribers(ctx, &nip47.Notification{ + notifier.notifySubscribers(ctx, &Notification{ Notification: transaction, - NotificationType: nip47.PAYMENT_RECEIVED_NOTIFICATION, + NotificationType: PAYMENT_RECEIVED_NOTIFICATION, }, nostr.Tags{}) return nil } -func (notifier *Nip47Notifier) notifySubscribers(ctx context.Context, notification *nip47.Notification, tags nostr.Tags) { +func (notifier *Nip47Notifier) notifySubscribers(ctx context.Context, notification *Notification, tags nostr.Tags) { apps := []db.App{} // TODO: join apps and permissions - notifier.svc.db.Find(&apps) + notifier.db.Find(&apps) for _, app := range apps { - hasPermission, _, _ := notifier.svc.hasPermission(&app, nip47.NOTIFICATIONS_PERMISSION, 0) + hasPermission, _, _ := notifier.permissionsSvc.HasPermission(&app, permissions.NOTIFICATIONS_PERMISSION, 0) if !hasPermission { continue } @@ -76,15 +85,15 @@ func (notifier *Nip47Notifier) notifySubscribers(ctx context.Context, notificati } } -func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App, notification *nip47.Notification, tags nostr.Tags) { - notifier.svc.logger.WithFields(logrus.Fields{ +func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App, notification *Notification, tags nostr.Tags) { + logger.Logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).Info("Notifying subscriber") - ss, err := nip04.ComputeSharedSecret(app.NostrPubkey, notifier.svc.cfg.GetNostrSecretKey()) + ss, err := nip04.ComputeSharedSecret(app.NostrPubkey, notifier.keys.GetNostrSecretKey()) if err != nil { - notifier.svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).WithError(err).Error("Failed to compute shared secret") @@ -93,7 +102,7 @@ func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App payloadBytes, err := json.Marshal(notification) if err != nil { - notifier.svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).WithError(err).Error("Failed to stringify notification") @@ -101,7 +110,7 @@ func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App } msg, err := nip04.Encrypt(string(payloadBytes), ss) if err != nil { - notifier.svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).WithError(err).Error("Failed to encrypt notification payload") @@ -112,15 +121,15 @@ func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App allTags = append(allTags, tags...) event := &nostr.Event{ - PubKey: notifier.svc.cfg.GetNostrPublicKey(), + PubKey: notifier.keys.GetNostrPublicKey(), CreatedAt: nostr.Now(), - Kind: nip47.NOTIFICATION_KIND, + Kind: models.NOTIFICATION_KIND, Tags: allTags, Content: msg, } - err = event.Sign(notifier.svc.cfg.GetNostrSecretKey()) + err = event.Sign(notifier.keys.GetNostrSecretKey()) if err != nil { - notifier.svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).WithError(err).Error("Failed to sign event") @@ -129,13 +138,13 @@ func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App err = notifier.relay.Publish(ctx, *event) if err != nil { - notifier.svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).WithError(err).Error("Failed to publish notification") return } - notifier.svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).Info("Published notification event") diff --git a/nip47/notifications/nip47_notifier_test.go b/nip47/notifications/nip47_notifier_test.go new file mode 100644 index 000000000..efb45a17b --- /dev/null +++ b/nip47/notifications/nip47_notifier_test.go @@ -0,0 +1,136 @@ +package notifications + +import ( + "context" + "encoding/json" + "log" + "testing" + + "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/events" + "github.com/getAlby/nostr-wallet-connect/nip47/permissions" + "github.com/getAlby/nostr-wallet-connect/tests" + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip04" + "github.com/stretchr/testify/assert" +) + +func TestSendNotification(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + /*mockLn, err := NewMockLn() + assert.NoError(t, err) + svc, err := createTestService(mockLn) + assert.NoError(t, err)*/ + + app, ss, err := tests.CreateApp(svc) + assert.NoError(t, err) + + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + RequestMethod: permissions.NOTIFICATIONS_PERMISSION, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + nip47NotificationQueue := NewNip47NotificationQueue() + svc.EventPublisher.RegisterSubscriber(nip47NotificationQueue) + + testEvent := &events.Event{ + Event: "nwc_payment_received", + Properties: &events.PaymentReceivedEventProperties{ + PaymentHash: tests.MockPaymentHash, + Amount: uint64(tests.MockTransaction.Amount), + NodeType: "LDK", + }, + } + + svc.EventPublisher.Publish(testEvent) + + receivedEvent := <-nip47NotificationQueue.Channel() + assert.Equal(t, testEvent, receivedEvent) + + relay := NewMockRelay() + + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + + notifier := NewNip47Notifier(relay, svc.DB, svc.Cfg, svc.Keys, permissionsSvc, svc.LNClient) + notifier.ConsumeEvent(ctx, receivedEvent) + + assert.NotNil(t, relay.publishedEvent) + assert.NotEmpty(t, relay.publishedEvent.Content) + + decrypted, err := nip04.Decrypt(relay.publishedEvent.Content, ss) + assert.NoError(t, err) + unmarshalledResponse := Notification{ + Notification: &PaymentReceivedNotification{}, + } + + err = json.Unmarshal([]byte(decrypted), &unmarshalledResponse) + assert.NoError(t, err) + assert.Equal(t, PAYMENT_RECEIVED_NOTIFICATION, unmarshalledResponse.NotificationType) + + transaction := (unmarshalledResponse.Notification.(*PaymentReceivedNotification)) + assert.Equal(t, tests.MockTransaction.Type, transaction.Type) + assert.Equal(t, tests.MockTransaction.Invoice, transaction.Invoice) + assert.Equal(t, tests.MockTransaction.Description, transaction.Description) + assert.Equal(t, tests.MockTransaction.DescriptionHash, transaction.DescriptionHash) + assert.Equal(t, tests.MockTransaction.Preimage, transaction.Preimage) + assert.Equal(t, tests.MockTransaction.PaymentHash, transaction.PaymentHash) + assert.Equal(t, tests.MockTransaction.Amount, transaction.Amount) + assert.Equal(t, tests.MockTransaction.FeesPaid, transaction.FeesPaid) + assert.Equal(t, tests.MockTransaction.SettledAt, transaction.SettledAt) +} + +func TestSendNotificationNoPermission(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + _, _, err = tests.CreateApp(svc) + assert.NoError(t, err) + + nip47NotificationQueue := NewNip47NotificationQueue() + svc.EventPublisher.RegisterSubscriber(nip47NotificationQueue) + + testEvent := &events.Event{ + Event: "nwc_payment_received", + Properties: &events.PaymentReceivedEventProperties{ + PaymentHash: tests.MockPaymentHash, + Amount: uint64(tests.MockTransaction.Amount), + NodeType: "LDK", + }, + } + + svc.EventPublisher.Publish(testEvent) + + receivedEvent := <-nip47NotificationQueue.Channel() + assert.Equal(t, testEvent, receivedEvent) + + relay := NewMockRelay() + + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + + notifier := NewNip47Notifier(relay, svc.DB, svc.Cfg, svc.Keys, permissionsSvc, svc.LNClient) + notifier.ConsumeEvent(ctx, receivedEvent) + + assert.Nil(t, relay.publishedEvent) +} + +type mockRelay struct { + publishedEvent *nostr.Event +} + +func NewMockRelay() *mockRelay { + return &mockRelay{} +} + +func (relay *mockRelay) Publish(ctx context.Context, event nostr.Event) error { + log.Printf("Mock Publishing event %+v", event) + relay.publishedEvent = &event + return nil +} diff --git a/nip47/permissions/permissions.go b/nip47/permissions/permissions.go new file mode 100644 index 000000000..2f26b5f4a --- /dev/null +++ b/nip47/permissions/permissions.go @@ -0,0 +1,128 @@ +package permissions + +import ( + "fmt" + "slices" + "time" + + "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/events" + "github.com/getAlby/nostr-wallet-connect/logger" + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +// TODO: move other permissions here (e.g. all payment methods use pay_invoice) +const ( + NOTIFICATIONS_PERMISSION = "notifications" +) + +type permissionsService struct { + db *gorm.DB + eventPublisher events.EventPublisher +} + +// TODO: does this need to be a service? +type PermissionsService interface { + HasPermission(app *db.App, requestMethod string, amount uint64) (result bool, code string, message string) + GetBudgetUsage(appPermission *db.AppPermission) uint64 + GetPermittedMethods(app *db.App) []string +} + +func NewPermissionsService(db *gorm.DB, eventPublisher events.EventPublisher) *permissionsService { + return &permissionsService{ + db: db, + eventPublisher: eventPublisher, + } +} + +func (svc *permissionsService) HasPermission(app *db.App, requestMethod string, amountMsat uint64) (result bool, code string, message string) { + switch requestMethod { + case models.PAY_INVOICE_METHOD, models.PAY_KEYSEND_METHOD, models.MULTI_PAY_INVOICE_METHOD, models.MULTI_PAY_KEYSEND_METHOD: + requestMethod = models.PAY_INVOICE_METHOD + } + + appPermission := db.AppPermission{} + findPermissionResult := svc.db.Find(&appPermission, &db.AppPermission{ + AppId: app.ID, + RequestMethod: requestMethod, + }) + if findPermissionResult.RowsAffected == 0 { + // No permission for this request method + return false, models.ERROR_RESTRICTED, fmt.Sprintf("This app does not have permission to request %s", requestMethod) + } + expiresAt := appPermission.ExpiresAt + if expiresAt != nil && expiresAt.Before(time.Now()) { + logger.Logger.WithFields(logrus.Fields{ + "requestMethod": requestMethod, + "expiresAt": expiresAt.Unix(), + "appId": app.ID, + "pubkey": app.NostrPubkey, + }).Info("This pubkey is expired") + + return false, models.ERROR_EXPIRED, "This app has expired" + } + + if requestMethod == models.PAY_INVOICE_METHOD { + maxAmount := appPermission.MaxAmount + if maxAmount != 0 { + budgetUsage := svc.GetBudgetUsage(&appPermission) + + if budgetUsage+amountMsat/1000 > uint64(maxAmount) { + return false, models.ERROR_QUOTA_EXCEEDED, "Insufficient budget remaining to make payment" + } + } + } + return true, "", "" +} + +func (svc *permissionsService) GetBudgetUsage(appPermission *db.AppPermission) uint64 { + var result struct { + Sum uint64 + } + // 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)).Scan(&result) + return result.Sum +} + +func (svc *permissionsService) GetPermittedMethods(app *db.App) []string { + appPermissions := []db.AppPermission{} + svc.db.Find(&appPermissions, &db.AppPermission{ + AppId: app.ID, + }) + requestMethods := make([]string, 0, len(appPermissions)) + for _, appPermission := range appPermissions { + requestMethods = append(requestMethods, appPermission.RequestMethod) + } + if slices.Contains(requestMethods, models.PAY_INVOICE_METHOD) { + // all payment methods are tied to the pay_invoice permission + requestMethods = append(requestMethods, models.PAY_KEYSEND_METHOD, models.MULTI_PAY_INVOICE_METHOD, models.MULTI_PAY_KEYSEND_METHOD) + } + + return requestMethods +} + +func getStartOfBudget(budget_type string) time.Time { + now := time.Now() + switch budget_type { + case models.BUDGET_RENEWAL_DAILY: + // TODO: Use the location of the user, instead of the server + return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + case models.BUDGET_RENEWAL_WEEKLY: + weekday := now.Weekday() + var startOfWeek time.Time + if weekday == 0 { + startOfWeek = now.AddDate(0, 0, -6) + } else { + startOfWeek = now.AddDate(0, 0, -int(weekday)+1) + } + return time.Date(startOfWeek.Year(), startOfWeek.Month(), startOfWeek.Day(), 0, 0, 0, 0, startOfWeek.Location()) + case models.BUDGET_RENEWAL_MONTHLY: + return time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) + case models.BUDGET_RENEWAL_YEARLY: + return time.Date(now.Year(), time.January, 1, 0, 0, 0, 0, now.Location()) + default: //"never" + return time.Time{} + } +} diff --git a/nip47/permissions/permissions_test.go b/nip47/permissions/permissions_test.go new file mode 100644 index 000000000..9e7f19fb5 --- /dev/null +++ b/nip47/permissions/permissions_test.go @@ -0,0 +1,110 @@ +package permissions + +import ( + "testing" + "time" + + "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/getAlby/nostr-wallet-connect/tests" + "github.com/stretchr/testify/assert" +) + +func TestHasPermission_NoPermission(t *testing.T) { + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + permissionsSvc := NewPermissionsService(svc.DB, svc.EventPublisher) + result, code, message := permissionsSvc.HasPermission(app, models.PAY_INVOICE_METHOD, 100) + assert.False(t, result) + assert.Equal(t, models.ERROR_RESTRICTED, code) + assert.Equal(t, "This app does not have permission to request pay_invoice", message) +} + +func TestHasPermission_Expired(t *testing.T) { + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + budgetRenewal := "never" + expiresAt := time.Now().Add(-24 * time.Hour) + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + RequestMethod: models.PAY_INVOICE_METHOD, + MaxAmount: 100, + BudgetRenewal: budgetRenewal, + ExpiresAt: &expiresAt, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + permissionsSvc := NewPermissionsService(svc.DB, svc.EventPublisher) + result, code, message := permissionsSvc.HasPermission(app, models.PAY_INVOICE_METHOD, 100) + assert.False(t, result) + assert.Equal(t, models.ERROR_EXPIRED, code) + assert.Equal(t, "This app has expired", message) +} + +func TestHasPermission_Exceeded(t *testing.T) { + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + budgetRenewal := "never" + expiresAt := time.Now().Add(24 * time.Hour) + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + RequestMethod: models.PAY_INVOICE_METHOD, + MaxAmount: 10, + BudgetRenewal: budgetRenewal, + ExpiresAt: &expiresAt, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + permissionsSvc := NewPermissionsService(svc.DB, svc.EventPublisher) + result, code, message := permissionsSvc.HasPermission(app, models.PAY_INVOICE_METHOD, 100*1000) + assert.False(t, result) + assert.Equal(t, models.ERROR_QUOTA_EXCEEDED, code) + assert.Equal(t, "Insufficient budget remaining to make payment", message) +} + +func TestHasPermission_OK(t *testing.T) { + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + budgetRenewal := "never" + expiresAt := time.Now().Add(24 * time.Hour) + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + RequestMethod: models.PAY_INVOICE_METHOD, + MaxAmount: 10, + BudgetRenewal: budgetRenewal, + ExpiresAt: &expiresAt, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + permissionsSvc := NewPermissionsService(svc.DB, svc.EventPublisher) + result, code, message := permissionsSvc.HasPermission(app, models.PAY_INVOICE_METHOD, 10*1000) + assert.True(t, result) + assert.Empty(t, code) + assert.Empty(t, message) +} diff --git a/nip47/publish_nip47_info.go b/nip47/publish_nip47_info.go new file mode 100644 index 000000000..a64fe689a --- /dev/null +++ b/nip47/publish_nip47_info.go @@ -0,0 +1,28 @@ +package nip47 + +import ( + "context" + "fmt" + + "github.com/getAlby/nostr-wallet-connect/nip47/models" + "github.com/getAlby/nostr-wallet-connect/nip47/notifications" + "github.com/nbd-wtf/go-nostr" +) + +func (svc *nip47Service) PublishNip47Info(ctx context.Context, relay *nostr.Relay) error { + ev := &nostr.Event{} + ev.Kind = models.INFO_EVENT_KIND + ev.Content = models.CAPABILITIES + ev.CreatedAt = nostr.Now() + ev.PubKey = svc.keys.GetNostrPublicKey() + ev.Tags = nostr.Tags{[]string{"notifications", notifications.NOTIFICATION_TYPES}} + err := ev.Sign(svc.keys.GetNostrSecretKey()) + if err != nil { + return err + } + err = relay.Publish(ctx, *ev) + if err != nil { + return fmt.Errorf("nostr publish not successful: %s", err) + } + return nil +} diff --git a/nip47_notifier_test.go b/nip47_notifier_test.go deleted file mode 100644 index ce156952b..000000000 --- a/nip47_notifier_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "log" - "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" - "github.com/nbd-wtf/go-nostr/nip04" - "github.com/stretchr/testify/assert" -) - -func TestSendNotification(t *testing.T) { - ctx := context.TODO() - defer os.Remove(testDB) - mockLn, err := NewMockLn() - assert.NoError(t, err) - svc, err := createTestService(mockLn) - assert.NoError(t, err) - app, ss, err := createApp(svc) - assert.NoError(t, err) - - appPermission := &db.AppPermission{ - AppId: app.ID, - App: *app, - RequestMethod: nip47.NOTIFICATIONS_PERMISSION, - } - err = svc.db.Create(appPermission).Error - assert.NoError(t, err) - - svc.nip47NotificationQueue = nip47.NewNip47NotificationQueue(svc.logger) - svc.eventPublisher.RegisterSubscriber(svc.nip47NotificationQueue) - - testEvent := &events.Event{ - Event: "nwc_payment_received", - Properties: &events.PaymentReceivedEventProperties{ - PaymentHash: mockPaymentHash, - Amount: uint64(mockTransaction.Amount), - NodeType: "LDK", - }, - } - - svc.eventPublisher.Publish(testEvent) - - receivedEvent := <-svc.nip47NotificationQueue.Channel() - assert.Equal(t, testEvent, receivedEvent) - - relay := NewMockRelay() - - n := NewNip47Notifier(svc, relay) - n.ConsumeEvent(ctx, receivedEvent) - - assert.NotNil(t, relay.publishedEvent) - assert.NotEmpty(t, relay.publishedEvent.Content) - - decrypted, err := nip04.Decrypt(relay.publishedEvent.Content, ss) - assert.NoError(t, err) - 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.(*nip47.PaymentReceivedNotification)) - assert.Equal(t, mockTransaction.Type, transaction.Type) - assert.Equal(t, mockTransaction.Invoice, transaction.Invoice) - assert.Equal(t, mockTransaction.Description, transaction.Description) - assert.Equal(t, mockTransaction.DescriptionHash, transaction.DescriptionHash) - assert.Equal(t, mockTransaction.Preimage, transaction.Preimage) - assert.Equal(t, mockTransaction.PaymentHash, transaction.PaymentHash) - assert.Equal(t, mockTransaction.Amount, transaction.Amount) - assert.Equal(t, mockTransaction.FeesPaid, transaction.FeesPaid) - assert.Equal(t, mockTransaction.SettledAt, transaction.SettledAt) -} - -func TestSendNotificationNoPermission(t *testing.T) { - ctx := context.TODO() - defer os.Remove(testDB) - mockLn, err := NewMockLn() - assert.NoError(t, err) - svc, err := createTestService(mockLn) - assert.NoError(t, err) - _, _, err = createApp(svc) - assert.NoError(t, err) - - svc.nip47NotificationQueue = nip47.NewNip47NotificationQueue(svc.logger) - svc.eventPublisher.RegisterSubscriber(svc.nip47NotificationQueue) - - testEvent := &events.Event{ - Event: "nwc_payment_received", - Properties: &events.PaymentReceivedEventProperties{ - PaymentHash: mockPaymentHash, - Amount: uint64(mockTransaction.Amount), - NodeType: "LDK", - }, - } - - svc.eventPublisher.Publish(testEvent) - - receivedEvent := <-svc.nip47NotificationQueue.Channel() - assert.Equal(t, testEvent, receivedEvent) - - relay := NewMockRelay() - - n := NewNip47Notifier(svc, relay) - n.ConsumeEvent(ctx, receivedEvent) - - assert.Nil(t, relay.publishedEvent) -} - -type mockRelay struct { - publishedEvent *nostr.Event -} - -func NewMockRelay() *mockRelay { - return &mockRelay{} -} - -func (relay *mockRelay) Publish(ctx context.Context, event nostr.Event) error { - log.Printf("Mock Publishing event %+v", event) - relay.publishedEvent = &event - return nil -} diff --git a/screenshot_index.png b/screenshot_index.png deleted file mode 100644 index 519300dc9..000000000 Binary files a/screenshot_index.png and /dev/null differ diff --git a/screenshot_new.png b/screenshot_new.png deleted file mode 100644 index d22b2f5dc..000000000 Binary files a/screenshot_new.png and /dev/null differ diff --git a/service.go b/service.go deleted file mode 100644 index d552fa32a..000000000 --- a/service.go +++ /dev/null @@ -1,915 +0,0 @@ -package main - -import ( - "context" - "database/sql" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/http/pprof" - "os" - "path" - "path/filepath" - "slices" - "strconv" - "strings" - "sync" - "time" - - "github.com/adrg/xdg" - "github.com/nbd-wtf/go-nostr" - "github.com/nbd-wtf/go-nostr/nip04" - "github.com/sirupsen/logrus" - "gopkg.in/DataDog/dd-trace-go.v1/profiler" - - "github.com/glebarez/sqlite" - "github.com/joho/godotenv" - "github.com/kelseyhightower/envconfig" - "github.com/orandin/lumberjackrus" - "gorm.io/gorm" - - 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/cashu" - "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/nip47" -) - -const ( - logDir = "log" - logFilename = "nwc.log" -) - -// TODO: move to service/ -// TODO: do not expose service struct -type Service struct { - // 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 - ctx context.Context - wg *sync.WaitGroup - nip47NotificationQueue nip47.Nip47NotificationQueue - appCancelFn context.CancelFunc -} - -// TODO: move to service.go -func NewService(ctx context.Context) (*Service, error) { - // Load config from environment variables / .GetEnv() file - godotenv.Load(".env") - appConfig := &config.AppConfig{} - err := envconfig.Process("", appConfig) - if err != nil { - return nil, err - } - - logger := logrus.New() - logger.SetFormatter(&logrus.JSONFormatter{}) - logger.SetOutput(os.Stdout) - logLevel, err := strconv.Atoi(appConfig.LogLevel) - if err != nil { - logLevel = int(logrus.InfoLevel) - } - logger.SetLevel(logrus.Level(logLevel)) - - if appConfig.Workdir == "" { - appConfig.Workdir = filepath.Join(xdg.DataHome, "/alby-nwc") - logger.WithField("workdir", appConfig.Workdir).Info("No workdir specified, using default") - } - // make sure workdir exists - os.MkdirAll(appConfig.Workdir, os.ModePerm) - - fileLoggerHook, err := lumberjackrus.NewHook( - &lumberjackrus.LogFile{ - Filename: filepath.Join(appConfig.Workdir, logDir, logFilename), - MaxAge: 3, - MaxBackups: 3, - }, - logrus.InfoLevel, - &logrus.JSONFormatter{}, - nil, - ) - if err != nil { - return nil, err - } - logger.AddHook(fileLoggerHook) - - finishRestoreNode(logger, appConfig.Workdir) - - // If DATABASE_URI is a URI or a path, leave it unchanged. - // If it only contains a filename, prepend the workdir. - if !strings.HasPrefix(appConfig.DatabaseUri, "file:") { - databasePath, _ := filepath.Split(appConfig.DatabaseUri) - if databasePath == "" { - appConfig.DatabaseUri = filepath.Join(appConfig.Workdir, appConfig.DatabaseUri) - } - } - - var gormDB *gorm.DB - var sqlDb *sql.DB - gormDB, err = gorm.Open(sqlite.Open(appConfig.DatabaseUri), &gorm.Config{}) - if err != nil { - return nil, err - } - err = gormDB.Exec("PRAGMA foreign_keys=ON;").Error - if err != nil { - return nil, err - } - err = gormDB.Exec("PRAGMA auto_vacuum=FULL;").Error - if err != nil { - return nil, err - } - sqlDb, err = gormDB.DB() - if err != nil { - return nil, err - } - sqlDb.SetMaxOpenConns(1) - - err = migrations.Migrate(gormDB, appConfig, logger) - if err != nil { - logger.WithError(err).Error("Failed to migrate") - return nil, err - } - - cfg := config.NewConfig(gormDB, appConfig, logger) - - eventPublisher := events.NewEventPublisher(logger) - - if err != nil { - logger.WithError(err).Error("Failed to create Alby OAuth service") - return nil, err - } - - nip47NotificationQueue := nip47.NewNip47NotificationQueue(logger) - eventPublisher.RegisterSubscriber(nip47NotificationQueue) - - var wg sync.WaitGroup - svc := &Service{ - cfg: cfg, - db: gormDB, - ctx: ctx, - wg: &wg, - logger: logger, - eventPublisher: eventPublisher, - nip47NotificationQueue: nip47NotificationQueue, - albyOAuthSvc: alby.NewAlbyOAuthService(logger, cfg, cfg.GetEnv(), db.NewDBService(gormDB, logger)), - } - - eventPublisher.RegisterSubscriber(svc.albyOAuthSvc) - - eventPublisher.Publish(&events.Event{ - Event: "nwc_started", - }) - - if appConfig.GoProfilerAddr != "" { - startProfiler(ctx, appConfig.GoProfilerAddr) - } - - if appConfig.DdProfilerEnabled { - startDataDogProfiler(ctx) - } - - return svc, nil -} - -func (svc *Service) StopLNClient() error { - if svc.lnClient != nil { - 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{ - Event: "nwc_node_stop_failed", - Properties: map[string]interface{}{ - "error": fmt.Sprintf("%v", err), - }, - }) - return err - } - svc.logger.Info("Publishing node shutdown event") - svc.lnClient = nil - svc.eventPublisher.Publish(&events.Event{ - Event: "nwc_node_stopped", - }) - } - svc.logger.Info("LNClient stopped successfully") - return nil -} - -func (svc *Service) launchLNBackend(ctx context.Context, encryptionKey string) error { - err := svc.StopLNClient() - if err != nil { - return err - } - - lnBackend, _ := svc.cfg.Get("LNBackendType", "") - if lnBackend == "" { - return errors.New("no LNBackendType specified") - } - - 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 = lnd.NewLNDService(ctx, svc.logger, LNDAddress, LNDCertHex, LNDMacaroonHex) - case config.LDKBackendType: - Mnemonic, _ := svc.cfg.Get("Mnemonic", encryptionKey) - LDKWorkdir := path.Join(svc.cfg.GetEnv().Workdir, "ldk") - - 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.GetEnv().Workdir, "greenlight") - - 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.GetEnv().Workdir, "breez") - - lnClient, err = breez.NewBreezService(svc.logger, Mnemonic, BreezAPIKey, GreenlightInviteCode, BreezWorkdir) - case config.PhoenixBackendType: - PhoenixdAddress, _ := svc.cfg.Get("PhoenixdAddress", encryptionKey) - PhoenixdAuthorization, _ := svc.cfg.Get("PhoenixdAuthorization", encryptionKey) - - lnClient, err = phoenixd.NewPhoenixService(svc.logger, PhoenixdAddress, PhoenixdAuthorization) - case config.CashuBackendType: - cashuMintUrl, _ := svc.cfg.Get("CashuMintUrl", encryptionKey) - cashuWorkdir := path.Join(svc.cfg.GetEnv().Workdir, "cashu") - - lnClient, err = cashu.NewCashuService(svc.logger, cashuWorkdir, cashuMintUrl) - default: - svc.logger.Fatalf("Unsupported LNBackendType: %v", lnBackend) - } - if err != nil { - 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") - } - if info != nil && info.Pubkey != "" { - svc.eventPublisher.SetGlobalProperty("node_id", info.Pubkey) - } - - svc.eventPublisher.Publish(&events.Event{ - Event: "nwc_node_started", - Properties: map[string]interface{}{ - "node_type": lnBackend, - }, - }) - svc.lnClient = lnClient - return nil -} - -func (svc *Service) createFilters(identityPubkey string) nostr.Filters { - filter := nostr.Filter{ - Tags: nostr.TagMap{"p": []string{identityPubkey}}, - Kinds: []int{nip47.REQUEST_KIND}, - } - return []nostr.Filter{filter} -} - -func (svc *Service) noticeHandler(notice string) { - 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 { - nip47Notifier := NewNip47Notifier(svc, sub.Relay) - go func() { - for { - select { - case <-ctx.Done(): - // subscription ended - return - case event := <-svc.nip47NotificationQueue.Channel(): - nip47Notifier.ConsumeEvent(ctx, event) - } - } - }() - - go func() { - // block till EOS is received - <-sub.EndOfStoredEvents - 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") - }() - - <-ctx.Done() - - if sub.Relay.ConnectionError != nil { - svc.logger.WithField("connectionError", sub.Relay.ConnectionError).Error("Relay error") - return sub.Relay.ConnectionError - } - svc.logger.Info("Exiting subscription...") - return nil -} - -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 := db.ResponseEvent{NostrId: resp.ID, RequestId: requestEvent.ID, State: "received"} - err := svc.db.Create(&responseEvent).Error - if err != nil { - svc.logger.WithFields(logrus.Fields{ - "requestEventNostrId": requestEvent.NostrId, - "appId": appId, - "replyEventId": resp.ID, - }).Errorf("Failed to save response/reply event: %v", err) - return err - } - - err = sub.Relay.Publish(ctx, *resp) - if err != nil { - responseEvent.State = db.RESPONSE_EVENT_STATE_PUBLISH_FAILED - svc.logger.WithFields(logrus.Fields{ - "requestEventId": requestEvent.ID, - "requestNostrEventId": requestEvent.NostrId, - "appId": appId, - "responseEventId": responseEvent.ID, - "responseNostrEventId": resp.ID, - }).Errorf("Failed to publish reply: %v", err) - } else { - responseEvent.State = db.RESPONSE_EVENT_STATE_PUBLISH_CONFIRMED - responseEvent.RepliedAt = time.Now() - svc.logger.WithFields(logrus.Fields{ - "requestEventId": requestEvent.ID, - "requestNostrEventId": requestEvent.NostrId, - "appId": appId, - "responseEventId": responseEvent.ID, - "responseNostrEventId": resp.ID, - }).Info("Published reply") - } - - err = svc.db.Save(&responseEvent).Error - if err != nil { - svc.logger.WithFields(logrus.Fields{ - "requestEventId": requestEvent.ID, - "requestNostrEventId": requestEvent.NostrId, - "appId": appId, - "responseEventId": responseEvent.ID, - "responseNostrEventId": resp.ID, - }).Errorf("Failed to update response/reply event: %v", err) - return err - } - - return nil -} - -func (svc *Service) HandleEvent(ctx context.Context, sub *nostr.Subscription, event *nostr.Event) { - var nip47Response *nip47.Response - svc.logger.WithFields(logrus.Fields{ - "requestEventNostrId": event.ID, - "eventKind": event.Kind, - }).Info("Processing Event") - - ss, err := nip04.ComputeSharedSecret(event.PubKey, svc.cfg.GetNostrSecretKey()) - if err != nil { - svc.logger.WithFields(logrus.Fields{ - "requestEventNostrId": event.ID, - "eventKind": event.Kind, - }).Errorf("Failed to compute shared secret: %v", err) - return - } - - // store request event - requestEvent := db.RequestEvent{AppId: nil, NostrId: event.ID, State: db.REQUEST_EVENT_STATE_HANDLER_EXECUTING} - err = svc.db.Create(&requestEvent).Error - if err != nil { - 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 = &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{ - "requestEventNostrId": event.ID, - "eventKind": event.Kind, - }).Errorf("Failed to process event: %v", err) - } - svc.PublishEvent(ctx, sub, &requestEvent, resp, nil) - return - } - - app := db.App{} - err = svc.db.First(&app, &db.App{ - NostrPubkey: event.PubKey, - }).Error - if err != nil { - svc.logger.WithFields(logrus.Fields{ - "nostrPubkey": event.PubKey, - }).Errorf("Failed to find app for nostr pubkey: %v", err) - - 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{ - "requestEventNostrId": event.ID, - "eventKind": event.Kind, - }).Errorf("Failed to process event: %v", err) - } - svc.PublishEvent(ctx, sub, &requestEvent, resp, &app) - - requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR - err = svc.db.Save(&requestEvent).Error - if err != nil { - svc.logger.WithFields(logrus.Fields{ - "nostrPubkey": event.PubKey, - }).Errorf("Failed to save state to nostr event: %v", err) - } - return - } - - requestEvent.AppId = &app.ID - err = svc.db.Save(&requestEvent).Error - if err != nil { - svc.logger.WithFields(logrus.Fields{ - "nostrPubkey": event.PubKey, - }).Errorf("Failed to save app to nostr event: %v", err) - - 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{ - "requestEventNostrId": event.ID, - "eventKind": event.Kind, - }).Errorf("Failed to process event: %v", err) - } - svc.PublishEvent(ctx, sub, &requestEvent, resp, &app) - - requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR - err = svc.db.Save(&requestEvent).Error - if err != nil { - svc.logger.WithFields(logrus.Fields{ - "nostrPubkey": event.PubKey, - }).Errorf("Failed to save state to nostr event: %v", err) - } - - return - } - - 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.GetNostrSecretKey()) - if err != nil { - svc.logger.WithFields(logrus.Fields{ - "requestEventNostrId": event.ID, - "eventKind": event.Kind, - }).Errorf("Failed to process event: %v", err) - - requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR - err = svc.db.Save(&requestEvent).Error - if err != nil { - svc.logger.WithFields(logrus.Fields{ - "nostrPubkey": event.PubKey, - }).Errorf("Failed to save state to nostr event: %v", err) - } - - return - } - payload, err := nip04.Decrypt(event.Content, ss) - if err != nil { - 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{ - "requestEventNostrId": event.ID, - "eventKind": event.Kind, - }).Errorf("Failed to process event: %v", err) - - requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR - err = svc.db.Save(&requestEvent).Error - if err != nil { - svc.logger.WithFields(logrus.Fields{ - "nostrPubkey": event.PubKey, - }).Errorf("Failed to save state to nostr event: %v", err) - } - - return - } - nip47Request := &nip47.Request{} - err = json.Unmarshal([]byte(payload), nip47Request) - if err != nil { - svc.logger.WithFields(logrus.Fields{ - "requestEventNostrId": event.ID, - "eventKind": event.Kind, - }).Errorf("Failed to process event: %v", err) - - requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR - err = svc.db.Save(&requestEvent).Error - if err != nil { - svc.logger.WithFields(logrus.Fields{ - "nostrPubkey": event.PubKey, - }).Errorf("Failed to save state to nostr event: %v", err) - } - - return - } - - requestEvent.Method = nip47Request.Method - requestEvent.ContentData = payload - svc.db.Save(&requestEvent) // we ignore potential DB errors here as this only saves the method and content data - - // TODO: replace with a channel - // TODO: update all previous occurences of svc.PublishEvent to also use the channel - publishResponse := func(nip47Response *nip47.Response, tags nostr.Tags) { - resp, err := svc.createResponse(event, nip47Response, tags, ss) - if err != nil { - svc.logger.WithFields(logrus.Fields{ - "requestEventNostrId": event.ID, - "eventKind": event.Kind, - }).Errorf("Failed to create response: %v", err) - 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{ - "requestEventNostrId": event.ID, - "eventKind": event.Kind, - }).Errorf("Failed to publish event: %v", err) - requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR - } else { - requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_EXECUTED - } - } - err = svc.db.Save(&requestEvent).Error - if err != nil { - svc.logger.WithFields(logrus.Fields{ - "nostrPubkey": event.PubKey, - }).Errorf("Failed to save state to nostr event: %v", err) - } - } - - switch nip47Request.Method { - case nip47.MULTI_PAY_INVOICE_METHOD: - svc.HandleMultiPayInvoiceEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) - case nip47.MULTI_PAY_KEYSEND_METHOD: - svc.HandleMultiPayKeysendEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) - case nip47.PAY_INVOICE_METHOD: - svc.HandlePayInvoiceEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) - case nip47.PAY_KEYSEND_METHOD: - svc.HandlePayKeysendEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) - case nip47.GET_BALANCE_METHOD: - svc.HandleGetBalanceEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) - case nip47.MAKE_INVOICE_METHOD: - svc.HandleMakeInvoiceEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) - case nip47.LOOKUP_INVOICE_METHOD: - svc.HandleLookupInvoiceEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) - case nip47.LIST_TRANSACTIONS_METHOD: - svc.HandleListTransactionsEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) - case nip47.GET_INFO_METHOD: - svc.HandleGetInfoEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) - case nip47.SIGN_MESSAGE_METHOD: - svc.HandleSignMessageEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) - default: - svc.handleUnknownMethod(ctx, nip47Request, publishResponse) - } -} - -func (svc *Service) handleUnknownMethod(ctx context.Context, nip47Request *nip47.Request, publishResponse func(*nip47.Response, nostr.Tags)) { - publishResponse(&nip47.Response{ - ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_NOT_IMPLEMENTED, - Message: fmt.Sprintf("Unknown method: %s", nip47Request.Method), - }, - }, nostr.Tags{}) -} - -func (svc *Service) createResponse(initialEvent *nostr.Event, content interface{}, tags nostr.Tags, ss []byte) (result *nostr.Event, err error) { - payloadBytes, err := json.Marshal(content) - if err != nil { - return nil, err - } - msg, err := nip04.Encrypt(string(payloadBytes), ss) - if err != nil { - return nil, err - } - - allTags := nostr.Tags{[]string{"p", initialEvent.PubKey}, []string{"e", initialEvent.ID}} - allTags = append(allTags, tags...) - - resp := &nostr.Event{ - PubKey: svc.cfg.GetNostrPublicKey(), - CreatedAt: nostr.Now(), - Kind: nip47.RESPONSE_KIND, - Tags: allTags, - Content: msg, - } - err = resp.Sign(svc.cfg.GetNostrSecretKey()) - if err != nil { - return nil, err - } - return resp, nil -} - -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)) - for _, appPermission := range appPermissions { - requestMethods = append(requestMethods, appPermission.RequestMethod) - } - if slices.Contains(requestMethods, nip47.PAY_INVOICE_METHOD) { - // all payment methods are tied to the pay_invoice permission - requestMethods = append(requestMethods, nip47.PAY_KEYSEND_METHOD, nip47.MULTI_PAY_INVOICE_METHOD, nip47.MULTI_PAY_KEYSEND_METHOD) - } - - return requestMethods -} - -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{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - }).Errorf("Failed to decode nostr event: %v", err) - return &nip47.Response{ - ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_BAD_REQUEST, - Message: err.Error(), - }} - } - return nil -} - -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{ - "requestEventNostrId": requestNostrEventId, - "appId": app.ID, - "code": code, - "message": message, - }).Error("App does not have permission") - - svc.eventPublisher.Publish(&events.Event{ - Event: "nwc_permission_denied", - Properties: map[string]interface{}{ - "request_method": nip47Request.Method, - "app_name": app.Name, - // "app_pubkey": app.NostrPubkey, - "code": code, - "message": message, - }, - }) - - return &nip47.Response{ - ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: code, - Message: message, - }, - } - } - return nil -} - -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 := db.AppPermission{} - findPermissionResult := svc.db.Find(&appPermission, &db.AppPermission{ - AppId: app.ID, - RequestMethod: requestMethod, - }) - if findPermissionResult.RowsAffected == 0 { - // No permission for this request method - return false, nip47.ERROR_RESTRICTED, fmt.Sprintf("This app does not have permission to request %s", requestMethod) - } - expiresAt := appPermission.ExpiresAt - if expiresAt != nil && expiresAt.Before(time.Now()) { - svc.logger.WithFields(logrus.Fields{ - "requestMethod": requestMethod, - "expiresAt": expiresAt.Unix(), - "appId": app.ID, - "pubkey": app.NostrPubkey, - }).Info("This pubkey is expired") - - return false, nip47.ERROR_EXPIRED, "This app has expired" - } - - if requestMethod == nip47.PAY_INVOICE_METHOD { - maxAmount := appPermission.MaxAmount - if maxAmount != 0 { - budgetUsage := svc.GetBudgetUsage(&appPermission) - - if budgetUsage+amount/1000 > int64(maxAmount) { - return false, nip47.ERROR_QUOTA_EXCEEDED, "Insufficient budget remaining to make payment" - } - } - } - return true, "", "" -} - -// 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, utils.GetStartOfBudget(appPermission.BudgetRenewal, appPermission.App.CreatedAt)).Scan(&result) - return int64(result.Sum) -} - -func (svc *Service) PublishNip47Info(ctx context.Context, relay *nostr.Relay) error { - ev := &nostr.Event{} - ev.Kind = nip47.INFO_EVENT_KIND - ev.Content = nip47.CAPABILITIES - ev.CreatedAt = nostr.Now() - ev.PubKey = svc.cfg.GetNostrPublicKey() - ev.Tags = nostr.Tags{[]string{"notifications", nip47.NOTIFICATION_TYPES}} - err := ev.Sign(svc.cfg.GetNostrSecretKey()) - if err != nil { - return err - } - err = relay.Publish(ctx, *ev) - if err != nil { - return fmt.Errorf("nostr publish not successful: %s", err) - } - return nil -} - -func (svc *Service) GetLogFilePath() string { - return filepath.Join(svc.cfg.GetEnv().Workdir, logDir, logFilename) -} - -func finishRestoreNode(logger *logrus.Logger, workDir string) { - restoreDir := filepath.Join(workDir, "restore") - if restoreDirStat, err := os.Stat(restoreDir); err == nil && restoreDirStat.IsDir() { - logger.WithField("restoreDir", restoreDir).Infof("Restore directory found. Finishing Node restore") - - existingFiles, err := os.ReadDir(restoreDir) - if err != nil { - logger.WithError(err).Fatal("Failed to read WORK_DIR") - } - - for _, file := range existingFiles { - if file.Name() != "restore" { - err = os.RemoveAll(filepath.Join(workDir, file.Name())) - if err != nil { - logger.WithField("filename", file.Name()).WithError(err).Fatal("Failed to remove file") - } - logger.WithField("filename", file.Name()).Info("removed file") - } - } - - files, err := os.ReadDir(restoreDir) - if err != nil { - logger.WithError(err).Fatal("Failed to read restore directory") - } - for _, file := range files { - err = os.Rename(filepath.Join(restoreDir, file.Name()), filepath.Join(workDir, file.Name())) - if err != nil { - logger.WithField("filename", file.Name()).WithError(err).Fatal("Failed to move file") - } - logger.WithField("filename", file.Name()).Info("copied file from restore directory") - } - err = os.RemoveAll(restoreDir) - if err != nil { - logger.WithError(err).Fatal("Failed to remove restore directory") - } - logger.WithField("restoreDir", restoreDir).Info("removed restore directory") - } -} - -func startProfiler(ctx context.Context, addr string) { - mux := http.NewServeMux() - mux.HandleFunc("/debug/pprof/", pprof.Index) - mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) - mux.HandleFunc("/debug/pprof/profile", pprof.Profile) - mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) - mux.HandleFunc("/debug/pprof/trace", pprof.Trace) - - server := &http.Server{ - Addr: addr, - Handler: mux, - } - - go func() { - <-ctx.Done() - err := server.Shutdown(context.Background()) - if err != nil { - panic("pprof server shutdown failed: " + err.Error()) - } - }() - - go func() { - err := server.ListenAndServe() - if err != nil && !errors.Is(err, http.ErrServerClosed) { - panic("pprof server failed: " + err.Error()) - } - }() -} - -func startDataDogProfiler(ctx context.Context) { - opts := make([]profiler.Option, 0) - - opts = append(opts, profiler.WithProfileTypes( - profiler.CPUProfile, - profiler.HeapProfile, - // higher overhead - profiler.BlockProfile, - profiler.MutexProfile, - profiler.GoroutineProfile, - )) - - err := profiler.Start(opts...) - if err != nil { - panic("failed to start DataDog profiler: " + err.Error()) - } - - go func() { - <-ctx.Done() - profiler.Stop() - }() -} - -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/keys/keys.go b/service/keys/keys.go new file mode 100644 index 000000000..7135db4d9 --- /dev/null +++ b/service/keys/keys.go @@ -0,0 +1,55 @@ +package keys + +import ( + "github.com/getAlby/nostr-wallet-connect/config" + "github.com/getAlby/nostr-wallet-connect/logger" + "github.com/nbd-wtf/go-nostr" +) + +type Keys interface { + Init(cfg config.Config, encryptionKey string) error + // Wallet Service Nostr pubkey + GetNostrPublicKey() string + // Wallet Service Nostr secret key + GetNostrSecretKey() string +} + +type keys struct { + nostrSecretKey string + nostrPublicKey string +} + +func NewKeys() *keys { + return &keys{} +} + +func (keys *keys) Init(cfg config.Config, 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 { + logger.Logger.WithError(err).Error("Error converting nostr privkey to pubkey") + return err + } + keys.nostrSecretKey = nostrSecretKey + keys.nostrPublicKey = nostrPublicKey + return nil +} + +func (keys *keys) GetNostrPublicKey() string { + if keys.nostrPublicKey == "" { + logger.Logger.Fatal("keys not initialized") + } + return keys.nostrPublicKey +} + +func (keys *keys) GetNostrSecretKey() string { + if keys.nostrSecretKey == "" { + logger.Logger.Fatal("keys not initialized") + } + return keys.nostrSecretKey +} diff --git a/service/models.go b/service/models.go index 59ecbf9dd..8bdb7c3f4 100644 --- a/service/models.go +++ b/service/models.go @@ -3,18 +3,23 @@ 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/events" "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/service/keys" + "gorm.io/gorm" ) 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 + WaitShutdown() + + // TODO: remove getters (currently used by http / wails services) GetAlbyOAuthSvc() alby.AlbyOAuthService + GetEventPublisher() events.EventPublisher + GetLNClient() lnclient.LNClient + GetDB() *gorm.DB + GetConfig() config.Config + GetKeys() keys.Keys } diff --git a/service/profiler.go b/service/profiler.go new file mode 100644 index 000000000..fa59a3ea4 --- /dev/null +++ b/service/profiler.go @@ -0,0 +1,63 @@ +package service + +import ( + "context" + + "errors" + "net/http" + "net/http/pprof" + + "gopkg.in/DataDog/dd-trace-go.v1/profiler" +) + +func startProfiler(ctx context.Context, addr string) { + mux := http.NewServeMux() + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + + server := &http.Server{ + Addr: addr, + Handler: mux, + } + + go func() { + <-ctx.Done() + err := server.Shutdown(context.Background()) + if err != nil { + panic("pprof server shutdown failed: " + err.Error()) + } + }() + + go func() { + err := server.ListenAndServe() + if err != nil && !errors.Is(err, http.ErrServerClosed) { + panic("pprof server failed: " + err.Error()) + } + }() +} + +func startDataDogProfiler(ctx context.Context) { + opts := make([]profiler.Option, 0) + + opts = append(opts, profiler.WithProfileTypes( + profiler.CPUProfile, + profiler.HeapProfile, + // higher overhead + profiler.BlockProfile, + profiler.MutexProfile, + profiler.GoroutineProfile, + )) + + err := profiler.Start(opts...) + if err != nil { + panic("failed to start DataDog profiler: " + err.Error()) + } + + go func() { + <-ctx.Done() + profiler.Stop() + }() +} diff --git a/service/service.go b/service/service.go new file mode 100644 index 000000000..9a7b7af59 --- /dev/null +++ b/service/service.go @@ -0,0 +1,247 @@ +package service + +import ( + "context" + "time" + + "os" + "path/filepath" + "strings" + "sync" + + "github.com/adrg/xdg" + "github.com/nbd-wtf/go-nostr" + "gorm.io/gorm" + + "github.com/joho/godotenv" + "github.com/kelseyhightower/envconfig" + + "github.com/getAlby/nostr-wallet-connect/alby" + "github.com/getAlby/nostr-wallet-connect/events" + "github.com/getAlby/nostr-wallet-connect/logger" + "github.com/getAlby/nostr-wallet-connect/service/keys" + + "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/nip47" + "github.com/getAlby/nostr-wallet-connect/nip47/models" +) + +type service struct { + cfg config.Config + + db *gorm.DB + lnClient lnclient.LNClient + albyOAuthSvc alby.AlbyOAuthService + eventPublisher events.EventPublisher + ctx context.Context + wg *sync.WaitGroup + nip47Service nip47.Nip47Service + appCancelFn context.CancelFunc + keys keys.Keys +} + +func NewService(ctx context.Context) (*service, error) { + // Load config from environment variables / .GetEnv() file + godotenv.Load(".env") + appConfig := &config.AppConfig{} + err := envconfig.Process("", appConfig) + if err != nil { + return nil, err + } + + logger.Init(appConfig.LogLevel) + + if appConfig.Workdir == "" { + appConfig.Workdir = filepath.Join(xdg.DataHome, "/alby-nwc") + logger.Logger.WithField("workdir", appConfig.Workdir).Info("No workdir specified, using default") + } + // make sure workdir exists + os.MkdirAll(appConfig.Workdir, os.ModePerm) + + err = logger.AddFileLogger(appConfig.Workdir) + if err != nil { + return nil, err + } + + finishRestoreNode(appConfig.Workdir) + + // If DATABASE_URI is a URI or a path, leave it unchanged. + // If it only contains a filename, prepend the workdir. + if !strings.HasPrefix(appConfig.DatabaseUri, "file:") { + databasePath, _ := filepath.Split(appConfig.DatabaseUri) + if databasePath == "" { + appConfig.DatabaseUri = filepath.Join(appConfig.Workdir, appConfig.DatabaseUri) + } + } + + gormDB, err := db.NewDB(appConfig.DatabaseUri) + if err != nil { + return nil, err + } + + cfg := config.NewConfig(appConfig, gormDB) + + eventPublisher := events.NewEventPublisher() + + if err != nil { + logger.Logger.WithError(err).Error("Failed to create Alby OAuth service") + return nil, err + } + + keys := keys.NewKeys() + + var wg sync.WaitGroup + svc := &service{ + cfg: cfg, + ctx: ctx, + wg: &wg, + eventPublisher: eventPublisher, + albyOAuthSvc: alby.NewAlbyOAuthService(gormDB, cfg, keys), + nip47Service: nip47.NewNip47Service(gormDB, cfg, keys, eventPublisher), + db: gormDB, + keys: keys, + } + + eventPublisher.RegisterSubscriber(svc.albyOAuthSvc) + + eventPublisher.Publish(&events.Event{ + Event: "nwc_started", + }) + + if appConfig.GoProfilerAddr != "" { + startProfiler(ctx, appConfig.GoProfilerAddr) + } + + if appConfig.DdProfilerEnabled { + startDataDogProfiler(ctx) + } + + return svc, nil +} + +func (svc *service) createFilters(identityPubkey string) nostr.Filters { + filter := nostr.Filter{ + Tags: nostr.TagMap{"p": []string{identityPubkey}}, + Kinds: []int{models.REQUEST_KIND}, + } + return []nostr.Filter{filter} +} + +func (svc *service) noticeHandler(notice string) { + logger.Logger.Infof("Received a notice %s", notice) +} + +func (svc *service) StartSubscription(ctx context.Context, sub *nostr.Subscription) error { + svc.nip47Service.StartNotifier(ctx, sub.Relay, svc.lnClient) + + go func() { + // block till EOS is received + <-sub.EndOfStoredEvents + logger.Logger.Info("Received EOS") + + // loop through incoming events + for event := range sub.Events { + go svc.nip47Service.HandleEvent(ctx, sub, event, svc.lnClient) + } + logger.Logger.Info("Relay subscription events channel ended") + }() + + <-ctx.Done() + + if sub.Relay.ConnectionError != nil { + logger.Logger.WithField("connectionError", sub.Relay.ConnectionError).Error("Relay error") + return sub.Relay.ConnectionError + } + logger.Logger.Info("Exiting subscription...") + return nil +} + +func finishRestoreNode(workDir string) { + restoreDir := filepath.Join(workDir, "restore") + if restoreDirStat, err := os.Stat(restoreDir); err == nil && restoreDirStat.IsDir() { + logger.Logger.WithField("restoreDir", restoreDir).Infof("Restore directory found. Finishing Node restore") + + existingFiles, err := os.ReadDir(restoreDir) + if err != nil { + logger.Logger.WithError(err).Fatal("Failed to read WORK_DIR") + } + + for _, file := range existingFiles { + if file.Name() != "restore" { + err = os.RemoveAll(filepath.Join(workDir, file.Name())) + if err != nil { + logger.Logger.WithField("filename", file.Name()).WithError(err).Fatal("Failed to remove file") + } + logger.Logger.WithField("filename", file.Name()).Info("removed file") + } + } + + files, err := os.ReadDir(restoreDir) + if err != nil { + logger.Logger.WithError(err).Fatal("Failed to read restore directory") + } + for _, file := range files { + err = os.Rename(filepath.Join(restoreDir, file.Name()), filepath.Join(workDir, file.Name())) + if err != nil { + logger.Logger.WithField("filename", file.Name()).WithError(err).Fatal("Failed to move file") + } + logger.Logger.WithField("filename", file.Name()).Info("copied file from restore directory") + } + err = os.RemoveAll(restoreDir) + if err != nil { + logger.Logger.WithError(err).Fatal("Failed to remove restore directory") + } + logger.Logger.WithField("restoreDir", restoreDir).Info("removed restore directory") + } +} + +func (svc *service) Shutdown() { + svc.StopLNClient() + svc.eventPublisher.Publish(&events.Event{ + Event: "nwc_stopped", + }) + // wait for any remaining events + time.Sleep(1 * time.Second) +} + +func (svc *service) StopApp() { + if svc.appCancelFn != nil { + svc.appCancelFn() + svc.wg.Wait() + } +} + +func (svc *service) GetDB() *gorm.DB { + return svc.db +} + +func (svc *service) GetConfig() config.Config { + return svc.cfg +} + +func (svc *service) GetAlbyOAuthSvc() alby.AlbyOAuthService { + return svc.albyOAuthSvc +} + +func (svc *service) GetNip47Service() nip47.Nip47Service { + return svc.nip47Service +} + +func (svc *service) GetEventPublisher() events.EventPublisher { + return svc.eventPublisher +} + +func (svc *service) GetLNClient() lnclient.LNClient { + return svc.lnClient +} + +func (svc *service) GetKeys() keys.Keys { + return svc.keys +} + +func (svc *service) WaitShutdown() { + logger.Logger.Info("Waiting for service to exit...") + svc.wg.Wait() +} diff --git a/service/start.go b/service/start.go new file mode 100644 index 000000000..17009f9d5 --- /dev/null +++ b/service/start.go @@ -0,0 +1,210 @@ +package service + +import ( + "context" + "errors" + "path" + "time" + + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip19" + "github.com/sirupsen/logrus" + + "github.com/getAlby/nostr-wallet-connect/config" + "github.com/getAlby/nostr-wallet-connect/events" + "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/lnclient/breez" + "github.com/getAlby/nostr-wallet-connect/lnclient/cashu" + "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/logger" +) + +func (svc *service) StartNostr(ctx context.Context, encryptionKey string) error { + + relayUrl := svc.cfg.GetRelayUrl() + + err := svc.keys.Init(svc.cfg, encryptionKey) + if err != nil { + logger.Logger.WithError(err).Fatal("Failed to init nostr keys") + } + + npub, err := nip19.EncodePublicKey(svc.keys.GetNostrPublicKey()) + if err != nil { + logger.Logger.WithError(err).Fatal("Error converting nostr privkey to pubkey") + } + + logger.Logger.WithFields(logrus.Fields{ + "npub": npub, + "hex": svc.keys.GetNostrPublicKey(), + }).Info("Starting nostr-wallet-connect") + svc.wg.Add(1) + go func() { + //Start infinite loop which will be only broken by canceling ctx (SIGINT) + var relay *nostr.Relay + + for i := 0; ; i++ { + // wait for a delay before retrying except on first iteration + if i > 0 { + sleepDuration := 10 + contextCancelled := false + logger.Logger.Infof("[Iteration %d] Retrying in %d seconds...", i, sleepDuration) + + select { + case <-ctx.Done(): //context cancelled + logger.Logger.Info("service context cancelled while waiting for retry") + contextCancelled = true + case <-time.After(time.Duration(sleepDuration) * time.Second): //timeout + } + if contextCancelled { + break + } + } + if relay != nil && relay.IsConnected() { + err := relay.Close() + if err != nil { + logger.Logger.WithError(err).Error("Could not close relay connection") + } + } + + //connect to the relay + logger.Logger.Infof("Connecting to the relay: %s", relayUrl) + + relay, err = nostr.RelayConnect(ctx, relayUrl, nostr.WithNoticeHandler(svc.noticeHandler)) + if err != nil { + logger.Logger.WithError(err).Error("Failed to connect to relay") + continue + } + + //publish event with NIP-47 info + err = svc.nip47Service.PublishNip47Info(ctx, relay) + if err != nil { + logger.Logger.WithError(err).Error("Could not publish NIP47 info") + } + + logger.Logger.Info("Subscribing to events") + sub, err := relay.Subscribe(ctx, svc.createFilters(svc.keys.GetNostrPublicKey())) + if err != nil { + logger.Logger.WithError(err).Error("Failed to subscribe to events") + continue + } + err = svc.StartSubscription(sub.Context, sub) + if err != nil { + //err being non-nil means that we have an error on the websocket error channel. In this case we just try to reconnect. + logger.Logger.WithError(err).Error("Got an error from the relay while listening to subscription.") + continue + } + //err being nil means that the context was canceled and we should exit the program. + break + } + logger.Logger.Info("Disconnecting from relay...") + if relay != nil && relay.IsConnected() { + err := relay.Close() + if err != nil { + logger.Logger.WithError(err).Error("Could not close relay connection") + } + } + svc.Shutdown() + logger.Logger.Info("Relay subroutine ended") + svc.wg.Done() + }() + return nil +} + +func (svc *service) StartApp(encryptionKey string) error { + if !svc.cfg.CheckUnlockPassword(encryptionKey) { + logger.Logger.Errorf("Invalid password") + return errors.New("invalid password") + } + + ctx, cancelFn := context.WithCancel(svc.ctx) + + err := svc.launchLNBackend(ctx, encryptionKey) + if err != nil { + logger.Logger.Errorf("Failed to launch LN backend: %v", err) + svc.eventPublisher.Publish(&events.Event{ + Event: "nwc_node_start_failed", + }) + cancelFn() + return err + } + + svc.StartNostr(ctx, encryptionKey) + svc.appCancelFn = cancelFn + return nil +} + +func (svc *service) launchLNBackend(ctx context.Context, encryptionKey string) error { + err := svc.StopLNClient() + if err != nil { + return err + } + + lnBackend, _ := svc.cfg.Get("LNBackendType", "") + if lnBackend == "" { + return errors.New("no LNBackendType specified") + } + + logger.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 = lnd.NewLNDService(ctx, LNDAddress, LNDCertHex, LNDMacaroonHex) + case config.LDKBackendType: + Mnemonic, _ := svc.cfg.Get("Mnemonic", encryptionKey) + LDKWorkdir := path.Join(svc.cfg.GetEnv().Workdir, "ldk") + + lnClient, err = ldk.NewLDKService(ctx, 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.GetEnv().Workdir, "greenlight") + + lnClient, err = greenlight.NewGreenlightService(svc.cfg, 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.GetEnv().Workdir, "breez") + + lnClient, err = breez.NewBreezService(Mnemonic, BreezAPIKey, GreenlightInviteCode, BreezWorkdir) + case config.PhoenixBackendType: + PhoenixdAddress, _ := svc.cfg.Get("PhoenixdAddress", encryptionKey) + PhoenixdAuthorization, _ := svc.cfg.Get("PhoenixdAuthorization", encryptionKey) + + lnClient, err = phoenixd.NewPhoenixService(PhoenixdAddress, PhoenixdAuthorization) + case config.CashuBackendType: + cashuMintUrl, _ := svc.cfg.Get("CashuMintUrl", encryptionKey) + cashuWorkdir := path.Join(svc.cfg.GetEnv().Workdir, "cashu") + + lnClient, err = cashu.NewCashuService(cashuWorkdir, cashuMintUrl) + default: + logger.Logger.Fatalf("Unsupported LNBackendType: %v", lnBackend) + } + if err != nil { + logger.Logger.WithError(err).Error("Failed to launch LN backend") + return err + } + + info, err := lnClient.GetInfo(ctx) + if err != nil { + logger.Logger.WithError(err).Error("Failed to fetch node info") + } + if info != nil && info.Pubkey != "" { + svc.eventPublisher.SetGlobalProperty("node_id", info.Pubkey) + } + + svc.eventPublisher.Publish(&events.Event{ + Event: "nwc_node_started", + Properties: map[string]interface{}{ + "node_type": lnBackend, + }, + }) + svc.lnClient = lnClient + return nil +} diff --git a/service/stop.go b/service/stop.go new file mode 100644 index 000000000..84b25efd3 --- /dev/null +++ b/service/stop.go @@ -0,0 +1,34 @@ +package service + +import ( + "fmt" + + "github.com/getAlby/nostr-wallet-connect/events" + "github.com/getAlby/nostr-wallet-connect/logger" +) + +// TODO: this should happen on ctx.Done() rather than having to call manually +// see svc.appCancelFn and how svc.StartNostr works +func (svc *service) StopLNClient() error { + if svc.lnClient != nil { + logger.Logger.Info("Shutting down LDK client") + err := svc.lnClient.Shutdown() + if err != nil { + logger.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), + }, + }) + return err + } + logger.Logger.Info("Publishing node shutdown event") + svc.lnClient = nil + svc.eventPublisher.Publish(&events.Event{ + Event: "nwc_node_stopped", + }) + } + logger.Logger.Info("LNClient stopped successfully") + return nil +} diff --git a/service_test.go b/service_test.go deleted file mode 100644 index fd41d8554..000000000 --- a/service_test.go +++ /dev/null @@ -1,1342 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "os" - "testing" - "time" - - "github.com/glebarez/sqlite" - "github.com/nbd-wtf/go-nostr" - "github.com/nbd-wtf/go-nostr/nip04" - "github.com/sirupsen/logrus" - "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/nip47" -) - -const testDB = "test.db" - -const nip47GetBalanceJson = ` -{ - "method": "get_balance" -} -` - -const nip47GetInfoJson = ` -{ - "method": "get_info" -} -` - -const nip47LookupInvoiceJson = ` -{ - "method": "lookup_invoice", - "params": { - "payment_hash": "4ad9cd27989b514d868e755178378019903a8d78767e3fceb211af9dd00e7a94" - } -} -` - -const nip47MakeInvoiceJson = ` -{ - "method": "make_invoice", - "params": { - "amount": 1000, - "description": "[[\"text/identifier\",\"hello@getalby.com\"],[\"text/plain\",\"Sats for Alby\"]]", - "expiry": 3600 - } -} -` - -const nip47ListTransactionsJson = ` -{ - "method": "list_transactions", - "params": { - "from": 1693876973, - "until": 1694876973, - "limit": 10, - "offset": 0, - "type": "incoming" - } -} -` - -const nip47KeysendJson = ` -{ - "method": "pay_keysend", - "params": { - "amount": 123000, - "pubkey": "123pubkey", - "tlv_records": [{ - "type": 5482373484, - "value": "fajsn341414fq" - }] - } -} -` - -const nip47MultiPayKeysendJson = ` -{ - "method": "multi_pay_keysend", - "params": { - "keysends": [{ - "amount": 123000, - "pubkey": "123pubkey", - "tlv_records": [{ - "type": 5482373484, - "value": "fajsn341414fq" - }] - }, - { - "amount": 123000, - "pubkey": "123pubkey", - "tlv_records": [{ - "type": 5482373484, - "value": "fajsn341414fq" - }] - } - ] - } -} -` - -const nip47MultiPayKeysendOneOverflowingBudgetJson = ` -{ - "method": "multi_pay_keysend", - "params": { - "keysends": [{ - "amount": 123000, - "pubkey": "123pubkey", - "id": "customId", - "tlv_records": [{ - "type": 5482373484, - "value": "fajsn341414fq" - }] - }, - { - "amount": 500000, - "pubkey": "500pubkey", - "tlv_records": [{ - "type": 5482373484, - "value": "fajsn341414fq" - }] - } - ] - } -} -` - -const nip47PayJson = ` -{ - "method": "pay_invoice", - "params": { - "invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" - } -} -` - -const nip47MultiPayJson = ` -{ - "method": "multi_pay_invoice", - "params": { - "invoices": [{ - "invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" - }, - { - "invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" - } - ] - } -} -` - -const nip47MultiPayOneOverflowingBudgetJson = ` -{ - "method": "multi_pay_invoice", - "params": { - "invoices": [{ - "invoice": "lnbcrt5u1pjuywzppp5h69dt59cypca2wxu69sw8ga0g39a3yx7dqug5nthrw3rcqgfdu4qdqqcqzzsxqyz5vqsp5gzlpzszyj2k30qmpme7jsfzr24wqlvt9xdmr7ay34lfelz050krs9qyyssq038x07nh8yuv8hdpjh5y8kqp7zcd62ql9na9xh7pla44htjyy02sz23q7qm2tza6ct4ypljk54w9k9qsrsu95usk8ce726ytep6vhhsq9mhf9a" - }, - { - "invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" - } - ] - } -} -` - -const nip47MultiPayOneMalformedInvoiceJson = ` -{ - "method": "multi_pay_invoice", - "params": { - "invoices": [{ - "invoice": "", - "id": "invoiceId123" - }, - { - "invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" - } - ] - } -} -` - -const nip47PayWrongMethodJson = ` -{ - "method": "get_balance", - "params": { - "invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" - } -} -` -const nip47PayJsonNoInvoice = ` -{ - "method": "pay_invoice", - "params": { - "something": "else" - } -} -` - -// for the invoice: -// lnbcrt5u1pjuywzppp5h69dt59cypca2wxu69sw8ga0g39a3yx7dqug5nthrw3rcqgfdu4qdqqcqzzsxqyz5vqsp5gzlpzszyj2k30qmpme7jsfzr24wqlvt9xdmr7ay34lfelz050krs9qyyssq038x07nh8yuv8hdpjh5y8kqp7zcd62ql9na9xh7pla44htjyy02sz23q7qm2tza6ct4ypljk54w9k9qsrsu95usk8ce726ytep6vhhsq9mhf9a -const mockPaymentHash500 = "be8ad5d0b82071d538dcd160e3a3af444bd890de68388a4d771ba23c01096f2a" - -const mockInvoice = "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" -const mockPaymentHash = "320c2c5a1492ccfd5bc7aa4ad9b657d6aaec3cfcc0d1d98413a29af4ac772ccf" // for the above invoice -var mockNodeInfo = lnclient.NodeInfo{ - Alias: "bob", - Color: "#3399FF", - Pubkey: "123pubkey", - Network: "testnet", - BlockHeight: 12, - BlockHash: "123blockhash", -} - -var mockTime = time.Unix(1693876963, 0) -var mockTimeUnix = mockTime.Unix() - -var mockTransactions = []nip47.Transaction{ - { - Type: "incoming", - Invoice: mockInvoice, - Description: "mock invoice 1", - DescriptionHash: "hash1", - Preimage: "preimage1", - PaymentHash: "payment_hash_1", - Amount: 1000, - FeesPaid: 50, - SettledAt: &mockTimeUnix, - Metadata: map[string]interface{}{ - "key1": "value1", - "key2": 42, - }, - }, - { - Type: "incoming", - Invoice: mockInvoice, - Description: "mock invoice 2", - DescriptionHash: "hash2", - Preimage: "preimage2", - PaymentHash: "payment_hash_2", - Amount: 2000, - FeesPaid: 75, - SettledAt: &mockTimeUnix, - }, -} -var mockTransaction = &mockTransactions[0] - -// TODO: split each method into separate files (requires moving out of the main package) -// TODO: add E2E tests as well (currently the LNClient and relay are not tested) -// TODO: test a request cannot be processed twice -// TODO: test if an app doesn't exist it returns the right error code -// TODO: test data is stored in the database correctly - -func TestHasPermission_NoPermission(t *testing.T) { - defer os.Remove(testDB) - mockLn, err := NewMockLn() - assert.NoError(t, err) - svc, err := createTestService(mockLn) - assert.NoError(t, err) - - app, _, err := createApp(svc) - assert.NoError(t, err) - - result, code, message := svc.hasPermission(app, nip47.PAY_INVOICE_METHOD, 100) - assert.False(t, result) - assert.Equal(t, nip47.ERROR_RESTRICTED, code) - assert.Equal(t, "This app does not have permission to request pay_invoice", message) -} - -func TestHasPermission_Expired(t *testing.T) { - defer os.Remove(testDB) - mockLn, err := NewMockLn() - assert.NoError(t, err) - svc, err := createTestService(mockLn) - assert.NoError(t, err) - - app, _, err := createApp(svc) - assert.NoError(t, err) - - budgetRenewal := "never" - expiresAt := time.Now().Add(-24 * time.Hour) - appPermission := &db.AppPermission{ - AppId: app.ID, - App: *app, - RequestMethod: nip47.PAY_INVOICE_METHOD, - MaxAmount: 100, - BudgetRenewal: budgetRenewal, - ExpiresAt: &expiresAt, - } - err = svc.db.Create(appPermission).Error - assert.NoError(t, err) - - result, code, message := svc.hasPermission(app, nip47.PAY_INVOICE_METHOD, 100) - assert.False(t, result) - assert.Equal(t, nip47.ERROR_EXPIRED, code) - assert.Equal(t, "This app has expired", message) -} - -func TestHasPermission_Exceeded(t *testing.T) { - defer os.Remove(testDB) - mockLn, err := NewMockLn() - assert.NoError(t, err) - svc, err := createTestService(mockLn) - assert.NoError(t, err) - - app, _, err := createApp(svc) - assert.NoError(t, err) - - budgetRenewal := "never" - expiresAt := time.Now().Add(24 * time.Hour) - appPermission := &db.AppPermission{ - AppId: app.ID, - App: *app, - RequestMethod: nip47.PAY_INVOICE_METHOD, - MaxAmount: 10, - BudgetRenewal: budgetRenewal, - ExpiresAt: &expiresAt, - } - err = svc.db.Create(appPermission).Error - assert.NoError(t, err) - - result, code, message := svc.hasPermission(app, nip47.PAY_INVOICE_METHOD, 100*1000) - assert.False(t, result) - assert.Equal(t, nip47.ERROR_QUOTA_EXCEEDED, code) - assert.Equal(t, "Insufficient budget remaining to make payment", message) -} - -func TestHasPermission_OK(t *testing.T) { - defer os.Remove(testDB) - mockLn, err := NewMockLn() - assert.NoError(t, err) - svc, err := createTestService(mockLn) - assert.NoError(t, err) - - app, _, err := createApp(svc) - assert.NoError(t, err) - - budgetRenewal := "never" - expiresAt := time.Now().Add(24 * time.Hour) - appPermission := &db.AppPermission{ - AppId: app.ID, - App: *app, - RequestMethod: nip47.PAY_INVOICE_METHOD, - MaxAmount: 10, - BudgetRenewal: budgetRenewal, - ExpiresAt: &expiresAt, - } - err = svc.db.Create(appPermission).Error - assert.NoError(t, err) - - result, code, message := svc.hasPermission(app, nip47.PAY_INVOICE_METHOD, 10*1000) - assert.True(t, result) - assert.Empty(t, code) - assert.Empty(t, message) -} - -func TestCreateResponse(t *testing.T) { - defer os.Remove(testDB) - mockLn, err := NewMockLn() - assert.NoError(t, err) - svc, err := createTestService(mockLn) - assert.NoError(t, err) - - reqPrivateKey := nostr.GeneratePrivateKey() - reqPubkey, err := nostr.GetPublicKey(reqPrivateKey) - assert.NoError(t, err) - - reqEvent := &nostr.Event{ - Kind: nip47.REQUEST_KIND, - PubKey: reqPubkey, - Content: "1", - } - - reqEvent.ID = "12345" - - ss, err := nip04.ComputeSharedSecret(reqPubkey, svc.cfg.GetNostrSecretKey()) - assert.NoError(t, err) - - nip47Response := &nip47.Response{ - ResultType: nip47.GET_BALANCE_METHOD, - Result: nip47.BalanceResponse{ - Balance: 1000, - }, - } - res, err := svc.createResponse(reqEvent, nip47Response, nostr.Tags{}, ss) - 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.GetNostrPublicKey(), res.PubKey) - - decrypted, err := nip04.Decrypt(res.Content, ss) - assert.NoError(t, err) - 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.(*nip47.BalanceResponse)) -} - -func TestHandleEncryption(t *testing.T) {} - -func TestHandleMultiPayInvoiceEvent(t *testing.T) { - ctx := context.TODO() - defer os.Remove(testDB) - mockLn, err := NewMockLn() - assert.NoError(t, err) - svc, err := createTestService(mockLn) - assert.NoError(t, err) - app, ss, err := createApp(svc) - assert.NoError(t, err) - - request := &nip47.Request{} - err = json.Unmarshal([]byte(nip47MultiPayJson), request) - assert.NoError(t, err) - - // without permission - payload, err := nip04.Encrypt(nip47MultiPayJson, ss) - assert.NoError(t, err) - reqEvent := &nostr.Event{ - Kind: nip47.REQUEST_KIND, - PubKey: app.NostrPubkey, - Content: payload, - } - - reqEvent.ID = "multi_pay_invoice_without_permission" - requestEvent := &db.RequestEvent{ - NostrId: reqEvent.ID, - } - - responses := []*nip47.Response{} - dTags := []nostr.Tags{} - - publishResponse := func(response *nip47.Response, tags nostr.Tags) { - responses = append(responses, response) - dTags = append(dTags, tags) - } - - svc.HandleMultiPayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - - assert.Equal(t, 2, len(responses)) - assert.Equal(t, 2, len(dTags)) - for i := 0; i < len(responses); i++ { - assert.Equal(t, nip47.ERROR_RESTRICTED, responses[i].Error.Code) - assert.Equal(t, mockPaymentHash, dTags[i].GetFirst([]string{"d"}).Value()) - } - - // with permission - maxAmount := 1000 - budgetRenewal := "never" - expiresAt := time.Now().Add(24 * time.Hour) - appPermission := &db.AppPermission{ - AppId: app.ID, - App: *app, - RequestMethod: nip47.PAY_INVOICE_METHOD, - MaxAmount: maxAmount, - BudgetRenewal: budgetRenewal, - ExpiresAt: &expiresAt, - } - err = svc.db.Create(appPermission).Error - assert.NoError(t, err) - - reqEvent.ID = "multi_pay_invoice_with_permission" - requestEvent.NostrId = reqEvent.ID - 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.(nip47.PayResponse).Preimage, "123preimage") - assert.Equal(t, mockPaymentHash, dTags[i].GetFirst([]string{"d"}).Value()) - } - - // one malformed invoice - err = json.Unmarshal([]byte(nip47MultiPayOneMalformedInvoiceJson), request) - assert.NoError(t, err) - - payload, err = nip04.Encrypt(nip47MultiPayOneMalformedInvoiceJson, ss) - assert.NoError(t, err) - reqEvent.Content = payload - - reqEvent.ID = "multi_pay_invoice_with_one_malformed_invoice" - requestEvent.NostrId = reqEvent.ID - responses = []*nip47.Response{} - dTags = []nostr.Tags{} - svc.HandleMultiPayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - - assert.Equal(t, 2, len(responses)) - assert.Equal(t, "invoiceId123", dTags[0].GetFirst([]string{"d"}).Value()) - 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.(nip47.PayResponse).Preimage, "123preimage") - - // we've spent 369 till here in three payments - - // budget overflow - newMaxAmount := 500 - 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) - assert.NoError(t, err) - - payload, err = nip04.Encrypt(nip47MultiPayOneOverflowingBudgetJson, ss) - assert.NoError(t, err) - reqEvent.Content = payload - - reqEvent.ID = "multi_pay_invoice_with_budget_overflow" - requestEvent.NostrId = reqEvent.ID - responses = []*nip47.Response{} - dTags = []nostr.Tags{} - svc.HandleMultiPayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - - // might be flaky because the two requests run concurrently - // and there's more chance that the failed respons calls the - // 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.(nip47.PayResponse).Preimage, "123preimage") - assert.Equal(t, mockPaymentHash, dTags[1].GetFirst([]string{"d"}).Value()) -} - -func TestHandleMultiPayKeysendEvent(t *testing.T) { - - ctx := context.TODO() - defer os.Remove(testDB) - mockLn, err := NewMockLn() - assert.NoError(t, err) - svc, err := createTestService(mockLn) - assert.NoError(t, err) - app, ss, err := createApp(svc) - assert.NoError(t, err) - - request := &nip47.Request{} - err = json.Unmarshal([]byte(nip47MultiPayKeysendJson), request) - assert.NoError(t, err) - - // without permission - payload, err := nip04.Encrypt(nip47MultiPayKeysendJson, ss) - assert.NoError(t, err) - reqEvent := &nostr.Event{ - Kind: nip47.REQUEST_KIND, - PubKey: app.NostrPubkey, - Content: payload, - } - - reqEvent.ID = "multi_pay_keysend_without_permission" - requestEvent := &db.RequestEvent{ - NostrId: reqEvent.ID, - } - - responses := []*nip47.Response{} - dTags := []nostr.Tags{} - - publishResponse := func(response *nip47.Response, tags nostr.Tags) { - responses = append(responses, response) - dTags = append(dTags, tags) - } - - svc.HandleMultiPayKeysendEvent(ctx, request, requestEvent, app, publishResponse) - - assert.Equal(t, 2, len(responses)) - for i := 0; i < len(responses); i++ { - assert.Equal(t, nip47.ERROR_RESTRICTED, responses[i].Error.Code) - } - - // with permission - maxAmount := 1000 - budgetRenewal := "never" - expiresAt := time.Now().Add(24 * time.Hour) - // 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 := &db.AppPermission{ - AppId: app.ID, - App: *app, - RequestMethod: nip47.PAY_INVOICE_METHOD, - MaxAmount: maxAmount, - BudgetRenewal: budgetRenewal, - ExpiresAt: &expiresAt, - } - err = svc.db.Create(appPermission).Error - assert.NoError(t, err) - - reqEvent.ID = "multi_pay_keysend_with_permission" - requestEvent.NostrId = reqEvent.ID - 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.(nip47.PayResponse).Preimage, "12345preimage") - assert.Equal(t, "123pubkey", dTags[i].GetFirst([]string{"d"}).Value()) - } - - // we've spent 246 till here in two payments - - // budget overflow - newMaxAmount := 500 - 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) - assert.NoError(t, err) - - payload, err = nip04.Encrypt(nip47MultiPayKeysendOneOverflowingBudgetJson, ss) - assert.NoError(t, err) - reqEvent.Content = payload - - reqEvent.ID = "multi_pay_keysend_with_budget_overflow" - requestEvent.NostrId = reqEvent.ID - 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.(nip47.PayResponse).Preimage, "12345preimage") - assert.Equal(t, "customId", dTags[1].GetFirst([]string{"d"}).Value()) -} - -func TestHandleGetBalanceEvent(t *testing.T) { - ctx := context.TODO() - defer os.Remove(testDB) - mockLn, err := NewMockLn() - assert.NoError(t, err) - svc, err := createTestService(mockLn) - assert.NoError(t, err) - app, ss, err := createApp(svc) - assert.NoError(t, err) - - request := &nip47.Request{} - err = json.Unmarshal([]byte(nip47GetBalanceJson), request) - assert.NoError(t, err) - - // without permission - payload, err := nip04.Encrypt(nip47GetBalanceJson, ss) - assert.NoError(t, err) - reqEvent := &nostr.Event{ - Kind: nip47.REQUEST_KIND, - PubKey: app.NostrPubkey, - Content: payload, - } - - reqEvent.ID = "test_get_balance_without_permission" - requestEvent := &db.RequestEvent{ - NostrId: reqEvent.ID, - } - - responses := []*nip47.Response{} - - publishResponse := func(response *nip47.Response, tags nostr.Tags) { - responses = append(responses, response) - } - - err = svc.db.Create(&requestEvent).Error - assert.NoError(t, err) - - svc.HandleGetBalanceEvent(ctx, request, requestEvent, app, publishResponse) - - assert.Equal(t, responses[0].Error.Code, nip47.ERROR_RESTRICTED) - - // with permission - expiresAt := time.Now().Add(24 * time.Hour) - appPermission := &db.AppPermission{ - AppId: app.ID, - App: *app, - RequestMethod: nip47.GET_BALANCE_METHOD, - ExpiresAt: &expiresAt, - } - err = svc.db.Create(appPermission).Error - assert.NoError(t, err) - - reqEvent.ID = "test_get_balance_with_permission" - requestEvent.NostrId = reqEvent.ID - responses = []*nip47.Response{} - svc.HandleGetBalanceEvent(ctx, request, requestEvent, app, publishResponse) - - assert.Equal(t, responses[0].Result.(*nip47.BalanceResponse).Balance, int64(21000)) - - // create pay_invoice permission - maxAmount := 1000 - budgetRenewal := "never" - appPermission = &db.AppPermission{ - AppId: app.ID, - App: *app, - RequestMethod: nip47.PAY_INVOICE_METHOD, - MaxAmount: maxAmount, - BudgetRenewal: budgetRenewal, - ExpiresAt: &expiresAt, - } - err = svc.db.Create(appPermission).Error - assert.NoError(t, err) - - reqEvent.ID = "test_get_balance_with_budget" - responses = []*nip47.Response{} - svc.HandleGetBalanceEvent(ctx, request, requestEvent, app, publishResponse) - - 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) { - ctx := context.TODO() - defer os.Remove(testDB) - mockLn, err := NewMockLn() - assert.NoError(t, err) - svc, err := createTestService(mockLn) - assert.NoError(t, err) - app, ss, err := createApp(svc) - assert.NoError(t, err) - - request := &nip47.Request{} - err = json.Unmarshal([]byte(nip47PayJson), request) - assert.NoError(t, err) - - // without permission - payload, err := nip04.Encrypt(nip47PayJson, ss) - assert.NoError(t, err) - reqEvent := &nostr.Event{ - Kind: nip47.REQUEST_KIND, - PubKey: app.NostrPubkey, - Content: payload, - } - - reqEvent.ID = "pay_invoice_without_permission" - requestEvent := &db.RequestEvent{ - NostrId: reqEvent.ID, - } - - responses := []*nip47.Response{} - - publishResponse := func(response *nip47.Response, tags nostr.Tags) { - responses = append(responses, response) - } - - svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - - assert.Equal(t, nip47.ERROR_RESTRICTED, responses[0].Error.Code) - - // with permission - maxAmount := 1000 - budgetRenewal := "never" - expiresAt := time.Now().Add(24 * time.Hour) - appPermission := &db.AppPermission{ - AppId: app.ID, - App: *app, - RequestMethod: nip47.PAY_INVOICE_METHOD, - MaxAmount: maxAmount, - BudgetRenewal: budgetRenewal, - ExpiresAt: &expiresAt, - } - err = svc.db.Create(appPermission).Error - assert.NoError(t, err) - - reqEvent.ID = "pay_invoice_with_permission" - requestEvent.NostrId = reqEvent.ID - responses = []*nip47.Response{} - svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - - assert.Equal(t, responses[0].Result.(nip47.PayResponse).Preimage, "123preimage") - - // malformed invoice - err = json.Unmarshal([]byte(nip47PayJsonNoInvoice), request) - assert.NoError(t, err) - - payload, err = nip04.Encrypt(nip47PayJsonNoInvoice, ss) - assert.NoError(t, err) - reqEvent.Content = payload - - reqEvent.ID = "pay_invoice_with_malformed_invoice" - requestEvent.NostrId = reqEvent.ID - responses = []*nip47.Response{} - svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - - assert.Equal(t, nip47.ERROR_INTERNAL, responses[0].Error.Code) - - // wrong method - err = json.Unmarshal([]byte(nip47PayWrongMethodJson), request) - assert.NoError(t, err) - - payload, err = nip04.Encrypt(nip47PayWrongMethodJson, ss) - assert.NoError(t, err) - reqEvent.Content = payload - - reqEvent.ID = "pay_invoice_with_wrong_request_method" - requestEvent.NostrId = reqEvent.ID - 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(&db.AppPermission{}).Where("app_id = ?", app.ID).Update("max_amount", newMaxAmount).Error - assert.NoError(t, err) - - err = json.Unmarshal([]byte(nip47PayJson), request) - assert.NoError(t, err) - - payload, err = nip04.Encrypt(nip47PayJson, ss) - assert.NoError(t, err) - reqEvent.Content = payload - - reqEvent.ID = "pay_invoice_with_budget_overflow" - requestEvent.NostrId = reqEvent.ID - 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(&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 = []*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(&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 = []*nip47.Response{} - svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - - assert.Equal(t, responses[0].Result.(nip47.PayResponse).Preimage, "123preimage") -} - -func TestHandlePayKeysendEvent(t *testing.T) { - ctx := context.TODO() - defer os.Remove(testDB) - mockLn, err := NewMockLn() - assert.NoError(t, err) - svc, err := createTestService(mockLn) - assert.NoError(t, err) - app, ss, err := createApp(svc) - assert.NoError(t, err) - - request := &nip47.Request{} - err = json.Unmarshal([]byte(nip47KeysendJson), request) - assert.NoError(t, err) - - // without permission - payload, err := nip04.Encrypt(nip47KeysendJson, ss) - assert.NoError(t, err) - reqEvent := &nostr.Event{ - Kind: nip47.REQUEST_KIND, - PubKey: app.NostrPubkey, - Content: payload, - } - - reqEvent.ID = "pay_keysend_without_permission" - requestEvent := &db.RequestEvent{ - NostrId: reqEvent.ID, - } - - responses := []*nip47.Response{} - - publishResponse := func(response *nip47.Response, tags nostr.Tags) { - responses = append(responses, response) - } - - svc.HandlePayKeysendEvent(ctx, request, requestEvent, app, publishResponse) - - assert.Equal(t, nip47.ERROR_RESTRICTED, responses[0].Error.Code) - - // with permission - maxAmount := 1000 - budgetRenewal := "never" - expiresAt := time.Now().Add(24 * time.Hour) - // 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 := &db.AppPermission{ - AppId: app.ID, - App: *app, - RequestMethod: nip47.PAY_INVOICE_METHOD, - MaxAmount: maxAmount, - BudgetRenewal: budgetRenewal, - ExpiresAt: &expiresAt, - } - err = svc.db.Create(appPermission).Error - assert.NoError(t, err) - - reqEvent.ID = "pay_keysend_with_permission" - requestEvent.NostrId = reqEvent.ID - responses = []*nip47.Response{} - svc.HandlePayKeysendEvent(ctx, request, requestEvent, app, publishResponse) - - assert.Equal(t, responses[0].Result.(nip47.PayResponse).Preimage, "12345preimage") - - // budget overflow - newMaxAmount := 100 - 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) - assert.NoError(t, err) - - payload, err = nip04.Encrypt(nip47KeysendJson, ss) - assert.NoError(t, err) - reqEvent.Content = payload - - reqEvent.ID = "pay_keysend_with_budget_overflow" - requestEvent.NostrId = reqEvent.ID - responses = []*nip47.Response{} - svc.HandlePayKeysendEvent(ctx, request, requestEvent, app, publishResponse) - - assert.Equal(t, nip47.ERROR_QUOTA_EXCEEDED, responses[0].Error.Code) -} - -func TestHandleLookupInvoiceEvent(t *testing.T) { - ctx := context.TODO() - defer os.Remove(testDB) - mockLn, err := NewMockLn() - assert.NoError(t, err) - svc, err := createTestService(mockLn) - assert.NoError(t, err) - app, ss, err := createApp(svc) - assert.NoError(t, err) - - request := &nip47.Request{} - err = json.Unmarshal([]byte(nip47LookupInvoiceJson), request) - assert.NoError(t, err) - - // without permission - payload, err := nip04.Encrypt(nip47LookupInvoiceJson, ss) - assert.NoError(t, err) - reqEvent := &nostr.Event{ - Kind: nip47.REQUEST_KIND, - PubKey: app.NostrPubkey, - Content: payload, - } - - reqEvent.ID = "test_lookup_invoice_without_permission" - requestEvent := &db.RequestEvent{ - NostrId: reqEvent.ID, - } - - responses := []*nip47.Response{} - - publishResponse := func(response *nip47.Response, tags nostr.Tags) { - responses = append(responses, response) - } - - svc.HandleLookupInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - - assert.Equal(t, nip47.ERROR_RESTRICTED, responses[0].Error.Code) - - // with permission - expiresAt := time.Now().Add(24 * time.Hour) - appPermission := &db.AppPermission{ - AppId: app.ID, - App: *app, - RequestMethod: nip47.LOOKUP_INVOICE_METHOD, - ExpiresAt: &expiresAt, - } - err = svc.db.Create(appPermission).Error - assert.NoError(t, err) - - reqEvent.ID = "test_lookup_invoice_with_permission" - requestEvent.NostrId = reqEvent.ID - responses = []*nip47.Response{} - svc.HandleLookupInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - - 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) - assert.Equal(t, mockTransaction.DescriptionHash, transaction.DescriptionHash) - assert.Equal(t, mockTransaction.Preimage, transaction.Preimage) - assert.Equal(t, mockTransaction.PaymentHash, transaction.PaymentHash) - assert.Equal(t, mockTransaction.Amount, transaction.Amount) - assert.Equal(t, mockTransaction.FeesPaid, transaction.FeesPaid) - assert.Equal(t, mockTransaction.SettledAt, transaction.SettledAt) -} - -func TestHandleMakeInvoiceEvent(t *testing.T) { - ctx := context.TODO() - defer os.Remove(testDB) - mockLn, err := NewMockLn() - assert.NoError(t, err) - svc, err := createTestService(mockLn) - assert.NoError(t, err) - app, ss, err := createApp(svc) - assert.NoError(t, err) - - request := &nip47.Request{} - err = json.Unmarshal([]byte(nip47MakeInvoiceJson), request) - assert.NoError(t, err) - - // without permission - payload, err := nip04.Encrypt(nip47MakeInvoiceJson, ss) - assert.NoError(t, err) - reqEvent := &nostr.Event{ - Kind: nip47.REQUEST_KIND, - PubKey: app.NostrPubkey, - Content: payload, - } - - reqEvent.ID = "test_make_invoice_without_permission" - requestEvent := &db.RequestEvent{ - NostrId: reqEvent.ID, - } - - responses := []*nip47.Response{} - - publishResponse := func(response *nip47.Response, tags nostr.Tags) { - responses = append(responses, response) - } - - svc.HandleMakeInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - - assert.Equal(t, nip47.ERROR_RESTRICTED, responses[0].Error.Code) - - // with permission - expiresAt := time.Now().Add(24 * time.Hour) - appPermission := &db.AppPermission{ - AppId: app.ID, - App: *app, - RequestMethod: nip47.MAKE_INVOICE_METHOD, - ExpiresAt: &expiresAt, - } - err = svc.db.Create(appPermission).Error - assert.NoError(t, err) - - reqEvent.ID = "test_make_invoice_with_permission" - requestEvent.NostrId = reqEvent.ID - responses = []*nip47.Response{} - svc.HandleMakeInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - - assert.Equal(t, mockTransaction.Preimage, responses[0].Result.(*nip47.MakeInvoiceResponse).Preimage) -} - -func TestHandleListTransactionsEvent(t *testing.T) { - ctx := context.TODO() - defer os.Remove(testDB) - mockLn, err := NewMockLn() - assert.NoError(t, err) - svc, err := createTestService(mockLn) - assert.NoError(t, err) - app, ss, err := createApp(svc) - assert.NoError(t, err) - - request := &nip47.Request{} - err = json.Unmarshal([]byte(nip47ListTransactionsJson), request) - assert.NoError(t, err) - - // without permission - payload, err := nip04.Encrypt(nip47ListTransactionsJson, ss) - assert.NoError(t, err) - reqEvent := &nostr.Event{ - Kind: nip47.REQUEST_KIND, - PubKey: app.NostrPubkey, - Content: payload, - } - - reqEvent.ID = "test_list_transactions_without_permission" - requestEvent := &db.RequestEvent{ - NostrId: reqEvent.ID, - } - - responses := []*nip47.Response{} - - publishResponse := func(response *nip47.Response, tags nostr.Tags) { - responses = append(responses, response) - } - - svc.HandleListTransactionsEvent(ctx, request, requestEvent, app, publishResponse) - - assert.Equal(t, nip47.ERROR_RESTRICTED, responses[0].Error.Code) - - // with permission - expiresAt := time.Now().Add(24 * time.Hour) - appPermission := &db.AppPermission{ - AppId: app.ID, - App: *app, - RequestMethod: nip47.LIST_TRANSACTIONS_METHOD, - ExpiresAt: &expiresAt, - } - err = svc.db.Create(appPermission).Error - assert.NoError(t, err) - - reqEvent.ID = "test_list_transactions_with_permission" - requestEvent.NostrId = reqEvent.ID - responses = []*nip47.Response{} - svc.HandleListTransactionsEvent(ctx, request, requestEvent, app, publishResponse) - - 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) - assert.Equal(t, mockTransactions[0].DescriptionHash, transaction.DescriptionHash) - assert.Equal(t, mockTransactions[0].Preimage, transaction.Preimage) - assert.Equal(t, mockTransactions[0].PaymentHash, transaction.PaymentHash) - assert.Equal(t, mockTransactions[0].Amount, transaction.Amount) - assert.Equal(t, mockTransactions[0].FeesPaid, transaction.FeesPaid) - assert.Equal(t, mockTransactions[0].SettledAt, transaction.SettledAt) -} - -func TestHandleGetInfoEvent(t *testing.T) { - ctx := context.TODO() - defer os.Remove(testDB) - mockLn, err := NewMockLn() - assert.NoError(t, err) - svc, err := createTestService(mockLn) - assert.NoError(t, err) - app, ss, err := createApp(svc) - assert.NoError(t, err) - - request := &nip47.Request{} - err = json.Unmarshal([]byte(nip47GetInfoJson), request) - assert.NoError(t, err) - - // without permission - payload, err := nip04.Encrypt(nip47GetInfoJson, ss) - assert.NoError(t, err) - reqEvent := &nostr.Event{ - Kind: nip47.REQUEST_KIND, - PubKey: app.NostrPubkey, - Content: payload, - } - - reqEvent.ID = "test_get_info_without_permission" - requestEvent := &db.RequestEvent{ - NostrId: reqEvent.ID, - } - - responses := []*nip47.Response{} - - publishResponse := func(response *nip47.Response, tags nostr.Tags) { - responses = append(responses, response) - } - - svc.HandleGetInfoEvent(ctx, request, requestEvent, app, publishResponse) - - assert.Equal(t, nip47.ERROR_RESTRICTED, responses[0].Error.Code) - - expiresAt := time.Now().Add(24 * time.Hour) - appPermission := &db.AppPermission{ - AppId: app.ID, - App: *app, - RequestMethod: nip47.GET_INFO_METHOD, - ExpiresAt: &expiresAt, - } - err = svc.db.Create(appPermission).Error - assert.NoError(t, err) - - reqEvent.ID = "test_get_info_with_permission" - requestEvent.NostrId = reqEvent.ID - responses = []*nip47.Response{} - svc.HandleGetInfoEvent(ctx, request, requestEvent, app, publishResponse) - - 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) - assert.Equal(t, mockNodeInfo.Network, nodeInfo.Network) - assert.Equal(t, mockNodeInfo.BlockHeight, nodeInfo.BlockHeight) - assert.Equal(t, mockNodeInfo.BlockHash, nodeInfo.BlockHash) - assert.Equal(t, []string{"get_info"}, nodeInfo.Methods) -} - -func createTestService(ln *MockLn) (svc *Service, err error) { - gormDb, err := gorm.Open(sqlite.Open(testDB), &gorm.Config{}) - if err != nil { - return nil, err - } - - logger := logrus.New() - logger.SetFormatter(&logrus.JSONFormatter{}) - logger.SetOutput(os.Stdout) - logger.SetLevel(logrus.InfoLevel) - - appConfig := &config.AppConfig{ - Workdir: ".test", - } - - err = migrations.Migrate(gormDb, appConfig, logger) - if err != nil { - return nil, err - } - - cfg := config.NewConfig( - gormDb, - appConfig, - logger, - ) - - cfg.Start("") - - return &Service{ - cfg: cfg, - db: gormDb, - lnClient: ln, - logger: logger, - eventPublisher: events.NewEventPublisher(logger), - }, nil -} - -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.GetNostrPublicKey(), senderPrivkey) - if err != nil { - return nil, nil, err - } - - app = &db.App{Name: "test", NostrPubkey: senderPubkey} - err = svc.db.Create(app).Error - if err != nil { - return nil, nil, err - } - - return app, ss, nil -} - -type MockLn struct { -} - -func NewMockLn() (*MockLn, error) { - return &MockLn{}, nil -} - -func (mln *MockLn) SendPaymentSync(ctx context.Context, payReq string) (*lnclient.PayInvoiceResponse, error) { - return &lnclient.PayInvoiceResponse{ - Preimage: "123preimage", - }, nil -} - -func (mln *MockLn) SendKeysend(ctx context.Context, amount int64, destination, preimage string, custom_records []lnclient.TLVRecord) (preImage string, err error) { - return "12345preimage", nil -} - -func (mln *MockLn) GetBalance(ctx context.Context) (balance int64, err error) { - return 21000, nil -} - -func (mln *MockLn) GetInfo(ctx context.Context) (info *lnclient.NodeInfo, err error) { - return &mockNodeInfo, nil -} - -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 *nip47.Transaction, err error) { - return mockTransaction, nil -} - -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 { - return nil -} - -func (mln *MockLn) ListChannels(ctx context.Context) (channels []lnclient.Channel, err error) { - return []lnclient.Channel{}, nil -} -func (mln *MockLn) GetNodeConnectionInfo(ctx context.Context) (nodeConnectionInfo *lnclient.NodeConnectionInfo, err error) { - return nil, nil -} -func (mln *MockLn) ConnectPeer(ctx context.Context, connectPeerRequest *lnclient.ConnectPeerRequest) error { - return nil -} -func (mln *MockLn) OpenChannel(ctx context.Context, openChannelRequest *lnclient.OpenChannelRequest) (*lnclient.OpenChannelResponse, error) { - return nil, nil -} -func (mln *MockLn) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) (*lnclient.CloseChannelResponse, error) { - return nil, nil -} -func (mln *MockLn) GetNewOnchainAddress(ctx context.Context) (string, error) { - return "", nil -} -func (mln *MockLn) GetBalances(ctx context.Context) (*lnclient.BalancesResponse, error) { - return nil, nil -} -func (mln *MockLn) GetOnchainBalance(ctx context.Context) (*lnclient.OnchainBalanceResponse, error) { - return nil, nil -} -func (mln *MockLn) RedeemOnchainFunds(ctx context.Context, toAddress string) (txId string, err error) { - return "", nil -} -func (mln *MockLn) ResetRouter(key string) error { - return nil -} -func (mln *MockLn) SendPaymentProbes(ctx context.Context, invoice string) error { - return nil -} -func (mln *MockLn) SendSpontaneousPaymentProbes(ctx context.Context, amountMsat uint64, nodeId string) error { - return nil -} -func (mln *MockLn) ListPeers(ctx context.Context) ([]lnclient.PeerDetails, error) { - return nil, nil -} -func (mln *MockLn) GetLogOutput(ctx context.Context, maxLen int) ([]byte, error) { - return []byte{}, nil -} -func (mln *MockLn) SignMessage(ctx context.Context, message string) (string, error) { - return "", nil -} -func (mln *MockLn) GetStorageDir() (string, error) { - return "", nil -} -func (mln *MockLn) GetNodeStatus(ctx context.Context) (nodeStatus *lnclient.NodeStatus, err error) { - return nil, nil -} -func (mln *MockLn) GetNetworkGraph(nodeIds []string) (lnclient.NetworkGraphResponse, error) { - return nil, nil -} -func (mln *MockLn) UpdateLastWalletSyncRequest() {} -func (mln *MockLn) DisconnectPeer(ctx context.Context, peerId string) error { - return nil -} diff --git a/start.go b/start.go deleted file mode 100644 index 90a17f3c1..000000000 --- a/start.go +++ /dev/null @@ -1,143 +0,0 @@ -package main - -import ( - "context" - "errors" - "time" - - "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.GetRelayUrl() - - err := svc.cfg.Start(encryptionKey) - if err != nil { - svc.logger.WithError(err).Fatal("Failed to start config") - } - - 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) - var relay *nostr.Relay - - for i := 0; ; i++ { - // wait for a delay before retrying except on first iteration - if i > 0 { - sleepDuration := 10 - contextCancelled := false - 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") - contextCancelled = true - case <-time.After(time.Duration(sleepDuration) * time.Second): //timeout - } - if contextCancelled { - break - } - } - if relay != nil && relay.IsConnected() { - err := relay.Close() - if err != nil { - svc.logger.WithError(err).Error("Could not close relay connection") - } - } - - //connect to the relay - 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") - 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.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") - 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.") - continue - } - //err being nil means that the context was canceled and we should exit the program. - break - } - 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.Shutdown() - svc.logger.Info("Relay subroutine ended") - svc.wg.Done() - }() - return nil -} - -func (svc *Service) StartApp(encryptionKey string) error { - if !svc.cfg.CheckUnlockPassword(encryptionKey) { - svc.logger.Errorf("Invalid password") - return errors.New("invalid password") - } - - ctx, cancelFn := context.WithCancel(svc.ctx) - - err := svc.launchLNBackend(ctx, encryptionKey) - if err != nil { - svc.logger.Errorf("Failed to launch LN backend: %v", err) - svc.eventPublisher.Publish(&events.Event{ - Event: "nwc_node_start_failed", - }) - cancelFn() - return err - } - - svc.StartNostr(ctx, encryptionKey) - svc.appCancelFn = cancelFn - return nil -} - -// TODO: remove and call StopLNClient() instead -func (svc *Service) StopApp() { - if svc.appCancelFn != nil { - svc.appCancelFn() - svc.wg.Wait() - } -} - -func (svc *Service) Shutdown() { - svc.StopLNClient() - svc.eventPublisher.Publish(&events.Event{ - Event: "nwc_stopped", - }) - // wait for any remaining events - time.Sleep(1 * time.Second) -} diff --git a/tests/create_app.go b/tests/create_app.go new file mode 100644 index 000000000..7acb676a3 --- /dev/null +++ b/tests/create_app.go @@ -0,0 +1,28 @@ +package tests + +import ( + "github.com/getAlby/nostr-wallet-connect/db" + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip04" +) + +func CreateApp(svc *TestService) (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.Keys.GetNostrPublicKey(), senderPrivkey) + if err != nil { + return nil, nil, err + } + + app = &db.App{Name: "test", NostrPubkey: senderPubkey} + err = svc.DB.Create(app).Error + if err != nil { + return nil, nil, err + } + + return app, ss, nil +} diff --git a/tests/mock_ln_client.go b/tests/mock_ln_client.go new file mode 100644 index 000000000..1805a416e --- /dev/null +++ b/tests/mock_ln_client.go @@ -0,0 +1,156 @@ +package tests + +import ( + "context" + "time" + + "github.com/getAlby/nostr-wallet-connect/lnclient" +) + +// for the invoice: +// lnbcrt5u1pjuywzppp5h69dt59cypca2wxu69sw8ga0g39a3yx7dqug5nthrw3rcqgfdu4qdqqcqzzsxqyz5vqsp5gzlpzszyj2k30qmpme7jsfzr24wqlvt9xdmr7ay34lfelz050krs9qyyssq038x07nh8yuv8hdpjh5y8kqp7zcd62ql9na9xh7pla44htjyy02sz23q7qm2tza6ct4ypljk54w9k9qsrsu95usk8ce726ytep6vhhsq9mhf9a +const MockPaymentHash500 = "be8ad5d0b82071d538dcd160e3a3af444bd890de68388a4d771ba23c01096f2a" + +const MockInvoice = "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" +const MockPaymentHash = "320c2c5a1492ccfd5bc7aa4ad9b657d6aaec3cfcc0d1d98413a29af4ac772ccf" // for the above invoice + +var MockNodeInfo = lnclient.NodeInfo{ + Alias: "bob", + Color: "#3399FF", + Pubkey: "123pubkey", + Network: "testnet", + BlockHeight: 12, + BlockHash: "123blockhash", +} + +var MockTime = time.Unix(1693876963, 0) +var MockTimeUnix = MockTime.Unix() + +var MockTransactions = []lnclient.Transaction{ + { + Type: "incoming", + Invoice: MockInvoice, + Description: "mock invoice 1", + DescriptionHash: "hash1", + Preimage: "preimage1", + PaymentHash: "payment_hash_1", + Amount: 1000, + FeesPaid: 50, + SettledAt: &MockTimeUnix, + Metadata: map[string]interface{}{ + "key1": "value1", + "key2": 42, + }, + }, + { + Type: "incoming", + Invoice: MockInvoice, + Description: "mock invoice 2", + DescriptionHash: "hash2", + Preimage: "preimage2", + PaymentHash: "payment_hash_2", + Amount: 2000, + FeesPaid: 75, + SettledAt: &MockTimeUnix, + }, +} +var MockTransaction = &MockTransactions[0] + +type MockLn struct { +} + +func NewMockLn() (*MockLn, error) { + return &MockLn{}, nil +} + +func (mln *MockLn) SendPaymentSync(ctx context.Context, payReq string) (*lnclient.PayInvoiceResponse, error) { + return &lnclient.PayInvoiceResponse{ + Preimage: "123preimage", + }, nil +} + +func (mln *MockLn) SendKeysend(ctx context.Context, amount uint64, destination, preimage string, custom_records []lnclient.TLVRecord) (preImage string, err error) { + return "12345preimage", nil +} + +func (mln *MockLn) GetBalance(ctx context.Context) (balance int64, err error) { + return 21000, nil +} + +func (mln *MockLn) GetInfo(ctx context.Context) (info *lnclient.NodeInfo, err error) { + return &MockNodeInfo, nil +} + +func (mln *MockLn) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *lnclient.Transaction, err error) { + return MockTransaction, nil +} + +func (mln *MockLn) LookupInvoice(ctx context.Context, paymentHash string) (transaction *lnclient.Transaction, err error) { + return MockTransaction, nil +} + +func (mln *MockLn) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (invoices []lnclient.Transaction, err error) { + return MockTransactions, nil +} +func (mln *MockLn) Shutdown() error { + return nil +} + +func (mln *MockLn) ListChannels(ctx context.Context) (channels []lnclient.Channel, err error) { + return []lnclient.Channel{}, nil +} +func (mln *MockLn) GetNodeConnectionInfo(ctx context.Context) (nodeConnectionInfo *lnclient.NodeConnectionInfo, err error) { + return nil, nil +} +func (mln *MockLn) ConnectPeer(ctx context.Context, connectPeerRequest *lnclient.ConnectPeerRequest) error { + return nil +} +func (mln *MockLn) OpenChannel(ctx context.Context, openChannelRequest *lnclient.OpenChannelRequest) (*lnclient.OpenChannelResponse, error) { + return nil, nil +} +func (mln *MockLn) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) (*lnclient.CloseChannelResponse, error) { + return nil, nil +} +func (mln *MockLn) GetNewOnchainAddress(ctx context.Context) (string, error) { + return "", nil +} +func (mln *MockLn) GetBalances(ctx context.Context) (*lnclient.BalancesResponse, error) { + return nil, nil +} +func (mln *MockLn) GetOnchainBalance(ctx context.Context) (*lnclient.OnchainBalanceResponse, error) { + return nil, nil +} +func (mln *MockLn) RedeemOnchainFunds(ctx context.Context, toAddress string) (txId string, err error) { + return "", nil +} +func (mln *MockLn) ResetRouter(key string) error { + return nil +} +func (mln *MockLn) SendPaymentProbes(ctx context.Context, invoice string) error { + return nil +} +func (mln *MockLn) SendSpontaneousPaymentProbes(ctx context.Context, amountMsat uint64, nodeId string) error { + return nil +} +func (mln *MockLn) ListPeers(ctx context.Context) ([]lnclient.PeerDetails, error) { + return nil, nil +} +func (mln *MockLn) GetLogOutput(ctx context.Context, maxLen int) ([]byte, error) { + return []byte{}, nil +} +func (mln *MockLn) SignMessage(ctx context.Context, message string) (string, error) { + return "", nil +} +func (mln *MockLn) GetStorageDir() (string, error) { + return "", nil +} +func (mln *MockLn) GetNodeStatus(ctx context.Context) (nodeStatus *lnclient.NodeStatus, err error) { + return nil, nil +} +func (mln *MockLn) GetNetworkGraph(nodeIds []string) (lnclient.NetworkGraphResponse, error) { + return nil, nil +} +func (mln *MockLn) UpdateLastWalletSyncRequest() {} +func (mln *MockLn) DisconnectPeer(ctx context.Context, peerId string) error { + return nil +} diff --git a/tests/test_service.go b/tests/test_service.go new file mode 100644 index 000000000..2d3c480a7 --- /dev/null +++ b/tests/test_service.go @@ -0,0 +1,63 @@ +package tests + +import ( + "os" + + "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/logger" + "github.com/getAlby/nostr-wallet-connect/service/keys" + "gorm.io/gorm" +) + +const testDB = "test.db" + +func CreateTestService() (svc *TestService, err error) { + gormDb, err := db.NewDB(testDB) + if err != nil { + return nil, err + } + + mockLn, err := NewMockLn() + if err != nil { + return nil, err + } + + logger.Init("") + + appConfig := &config.AppConfig{ + Workdir: ".test", + } + + cfg := config.NewConfig( + appConfig, + gormDb, + ) + + keys := keys.NewKeys() + keys.Init(cfg, "") + + eventPublisher := events.NewEventPublisher() + + return &TestService{ + Cfg: cfg, + LNClient: mockLn, + EventPublisher: eventPublisher, + DB: gormDb, + Keys: keys, + }, nil +} + +type TestService struct { + Keys keys.Keys + Cfg config.Config + LNClient lnclient.LNClient + EventPublisher events.EventPublisher + DB *gorm.DB +} + +func RemoveTestService() { + os.Remove(testDB) +} diff --git a/utils/utils.go b/utils/utils.go index edbceefbe..1c9c7b721 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -4,35 +4,8 @@ import ( "fmt" "io" "os" - "time" - - "github.com/getAlby/nostr-wallet-connect/nip47" ) -func GetStartOfBudget(budget_type string, createdAt time.Time) time.Time { - now := time.Now() - switch budget_type { - case nip47.BUDGET_RENEWAL_DAILY: - // TODO: Use the location of the user, instead of the server - return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) - case nip47.BUDGET_RENEWAL_WEEKLY: - weekday := now.Weekday() - var startOfWeek time.Time - if weekday == 0 { - startOfWeek = now.AddDate(0, 0, -6) - } else { - startOfWeek = now.AddDate(0, 0, -int(weekday)+1) - } - return time.Date(startOfWeek.Year(), startOfWeek.Month(), startOfWeek.Day(), 0, 0, 0, 0, startOfWeek.Location()) - case nip47.BUDGET_RENEWAL_MONTHLY: - return time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) - case nip47.BUDGET_RENEWAL_YEARLY: - return time.Date(now.Year(), time.January, 1, 0, 0, 0, 0, now.Location()) - default: //"never" - return createdAt - } -} - func ReadFileTail(filePath string, maxLen int) (data []byte, err error) { f, err := os.Open(filePath) if err != nil { diff --git a/wails/wails_app.go b/wails/wails_app.go new file mode 100644 index 000000000..001c109e3 --- /dev/null +++ b/wails/wails_app.go @@ -0,0 +1,105 @@ +package wails + +import ( + "context" + "embed" + "log" + + "github.com/getAlby/nostr-wallet-connect/api" + "github.com/getAlby/nostr-wallet-connect/logger" + "github.com/getAlby/nostr-wallet-connect/service" + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" + "github.com/wailsapp/wails/v2/pkg/options/linux" + "github.com/wailsapp/wails/v2/pkg/options/mac" + "gorm.io/gorm" +) + +type WailsApp struct { + ctx context.Context + svc service.Service + api api.API + db *gorm.DB +} + +func NewApp(svc service.Service) *WailsApp { + return &WailsApp{ + svc: svc, + api: api.NewAPI(svc, svc.GetDB(), svc.GetConfig(), svc.GetKeys(), svc.GetAlbyOAuthSvc(), svc.GetEventPublisher()), + db: svc.GetDB(), + } +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (app *WailsApp) startup(ctx context.Context) { + app.ctx = ctx +} + +func LaunchWailsApp(app *WailsApp, assets embed.FS, appIcon []byte) { + err := wails.Run(&options.App{ + Title: "AlbyHub", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + Logger: NewWailsLogger(), + // HideWindowOnClose: true, // with this on, there is no way to close the app - wait for v3 + + //BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + Mac: &mac.Options{ + About: &mac.AboutInfo{ + Title: "AlbyHub", + Icon: appIcon, + }, + }, + Linux: &linux.Options{ + Icon: appIcon, + }, + }) + + if err != nil { + log.Fatalf("Error %v", err) + } +} + +func NewWailsLogger() WailsLogger { + return WailsLogger{} +} + +type WailsLogger struct { +} + +func (wailsLogger WailsLogger) Print(message string) { + logger.Logger.WithField("wails", true).Print(message) +} + +func (wailsLogger WailsLogger) Trace(message string) { + logger.Logger.WithField("wails", true).Trace(message) +} + +func (wailsLogger WailsLogger) Debug(message string) { + logger.Logger.WithField("wails", true).Debug(message) +} + +func (wailsLogger WailsLogger) Info(message string) { + logger.Logger.WithField("wails", true).Info(message) +} + +func (wailsLogger WailsLogger) Warning(message string) { + logger.Logger.WithField("wails", true).Warning(message) +} + +func (wailsLogger WailsLogger) Error(message string) { + logger.Logger.WithField("wails", true).Error(message) +} + +func (wailsLogger WailsLogger) Fatal(message string) { + logger.Logger.WithField("wails", true).Fatal(message) +} diff --git a/wails_handlers.go b/wails/wails_handlers.go similarity index 89% rename from wails_handlers.go rename to wails/wails_handlers.go index 7829498c3..e8de2386a 100644 --- a/wails_handlers.go +++ b/wails/wails_handlers.go @@ -1,4 +1,4 @@ -package main +package wails import ( "encoding/json" @@ -12,7 +12,7 @@ import ( "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/getAlby/nostr-wallet-connect/logger" "github.com/wailsapp/wails/v2/pkg/runtime" ) @@ -36,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.GetAlbyOAuthSvc().CallbackHandler(ctx, code) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -58,8 +58,9 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string case len(appMatch) > 1: pubkey := appMatch[1] - userApp := db.App{} - findResult := app.svc.db.Where("nostr_pubkey = ?", pubkey).First(&userApp) + // TODO: move this to DB service + dbApp := db.App{} + findResult := app.db.Where("nostr_pubkey = ?", pubkey).First(&dbApp) if findResult.RowsAffected == 0 { return WailsRequestRouterResponse{Body: nil, Error: "App does not exist"} @@ -67,26 +68,26 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string switch method { case "GET": - app := app.api.GetApp(&userApp) + app := app.api.GetApp(&dbApp) return WailsRequestRouterResponse{Body: app, Error: ""} case "PATCH": updateAppRequest := &api.UpdateAppRequest{} err := json.Unmarshal([]byte(body), updateAppRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + logger.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.api.UpdateApp(&userApp, updateAppRequest) + err = app.api.UpdateApp(&dbApp, updateAppRequest) if err != nil { return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } return WailsRequestRouterResponse{Body: nil, Error: ""} case "DELETE": - err := app.api.DeleteApp(&userApp) + err := app.api.DeleteApp(&dbApp) if err != nil { return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -168,9 +169,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.GetAlbyOAuthSvc().GetMe(ctx) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -179,9 +180,9 @@ 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.GetAlbyOAuthSvc().GetBalance(ctx) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -195,14 +196,14 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string payRequest := &alby.AlbyPayRequest{} err := json.Unmarshal([]byte(body), payRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + logger.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.GetAlbyOAuthSvc().SendPayment(ctx, payRequest.Invoice) if err != nil { return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -219,7 +220,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{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -236,7 +237,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{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -270,7 +271,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{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -317,7 +318,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{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -340,13 +341,13 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string return WailsRequestRouterResponse{Body: *signMessageResponse, Error: ""} // TODO: review naming case "/api/instant-channel-invoices": - newInstantChannelRequest := &lsp.NewInstantChannelInvoiceRequest{} + newInstantChannelRequest := &api.NewInstantChannelInvoiceRequest{} err := json.Unmarshal([]byte(body), newInstantChannelRequest) - newInstantChannelResponseResponse, err := app.api.GetLSPService().NewInstantChannelInvoice(ctx, newInstantChannelRequest) + newInstantChannelResponse, err := app.api.NewInstantChannelInvoice(ctx, newInstantChannelRequest) if err != nil { return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } - return WailsRequestRouterResponse{Body: *newInstantChannelResponseResponse, Error: ""} + return WailsRequestRouterResponse{Body: *newInstantChannelResponse, Error: ""} case "/api/peers": switch method { case "GET": @@ -359,7 +360,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{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -393,7 +394,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.GetAlbyOAuthSvc().LinkAccount(ctx) if err != nil { return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -407,7 +408,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{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -417,7 +418,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{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -429,7 +430,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{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -439,7 +440,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{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -451,7 +452,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{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -460,7 +461,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{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -474,7 +475,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{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -483,7 +484,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{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -495,7 +496,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{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -504,7 +505,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{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -516,7 +517,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{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -525,7 +526,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{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -537,7 +538,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{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -550,7 +551,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string DefaultFilename: "nwc.bkp", }) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -560,7 +561,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{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -570,10 +571,10 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string defer backupFile.Close() - err = app.api.GetBackupService().CreateBackup(backupRequest.UnlockPassword, backupFile) + err = app.api.CreateBackup(backupRequest.UnlockPassword, backupFile) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -585,7 +586,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{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -598,7 +599,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string DefaultFilename: "nwc.bkp", }) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -608,7 +609,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{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -618,9 +619,9 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string defer backupFile.Close() - err = app.api.GetBackupService().RestoreBackup(restoreRequest.UnlockPassword, backupFile) + err = app.api.RestoreBackup(restoreRequest.UnlockPassword, backupFile) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -638,7 +639,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{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -647,7 +648,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{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -657,7 +658,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string return WailsRequestRouterResponse{Body: logOutputResponse, Error: ""} } - app.svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, }).Error("Unhandled route") diff --git a/wails_app.go b/wails_app.go deleted file mode 100644 index 5ffe3ca08..000000000 --- a/wails_app.go +++ /dev/null @@ -1,112 +0,0 @@ -package main - -import ( - "context" - "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" - "github.com/wailsapp/wails/v2/pkg/options/assetserver" - "github.com/wailsapp/wails/v2/pkg/options/linux" - "github.com/wailsapp/wails/v2/pkg/options/mac" -) - -//go:embed all:frontend/dist -var assets embed.FS - -//go:embed appicon.png -var appIcon []byte - -type WailsApp struct { - ctx context.Context - svc *Service - api api.API -} - -func NewApp(svc *Service) *WailsApp { - return &WailsApp{ - svc: svc, - api: api.NewAPI(svc, svc.logger, svc.db), - } -} - -// startup is called when the app starts. The context is saved -// so we can call the runtime methods -func (app *WailsApp) startup(ctx context.Context) { - app.ctx = ctx -} - -func LaunchWailsApp(app *WailsApp) { - logger := NewWailsLogger(app.svc.logger) - - err := wails.Run(&options.App{ - Title: "AlbyHub", - Width: 1024, - Height: 768, - AssetServer: &assetserver.Options{ - Assets: assets, - }, - // HideWindowOnClose: true, // with this on, there is no way to close the app - wait for v3 - Logger: logger, - - //BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, - OnStartup: app.startup, - Bind: []interface{}{ - app, - }, - Mac: &mac.Options{ - About: &mac.AboutInfo{ - Title: "AlbyHub", - Icon: appIcon, - }, - }, - Linux: &linux.Options{ - Icon: appIcon, - }, - }) - - if err != nil { - log.Fatalf("Error %v", err) - } -} - -func NewWailsLogger(appLogger *logrus.Logger) WailsLogger { - return WailsLogger{ - AppLogger: appLogger, - } -} - -type WailsLogger struct { - AppLogger *logrus.Logger -} - -func (logger WailsLogger) Print(message string) { - logger.AppLogger.Print(message) -} - -func (logger WailsLogger) Trace(message string) { - logger.AppLogger.Trace(message) -} - -func (logger WailsLogger) Debug(message string) { - logger.AppLogger.Debug(message) -} - -func (logger WailsLogger) Info(message string) { - logger.AppLogger.Info(message) -} - -func (logger WailsLogger) Warning(message string) { - logger.AppLogger.Warning(message) -} - -func (logger WailsLogger) Error(message string) { - logger.AppLogger.Error(message) -} - -func (logger WailsLogger) Fatal(message string) { - logger.AppLogger.Fatal(message) -}