From 3ed3ec5f1347f36b5097403a2641fcc396396ae0 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 12 Jun 2024 18:03:31 +0700 Subject: [PATCH 01/16] chore: split service into packages (WIP) --- README.md | 6 +- README_GREENLIGHT.md | 17 - api/api.go | 97 +- backup/backup_service.go => api/backup.go | 21 +- api/esplora.go | 2 +- lsp/lsp_service.go => api/lsp.go | 67 +- api/models.go | 19 +- backup/models.go | 10 - config/config.go | 21 +- db/db_service.go | 14 +- http/http_service.go | 11 +- lnclient/models.go | 1 - lsp/models.go | 24 - main_http.go | 20 +- main_wails.go | 24 +- nip47/budgets.go | 40 + nip47/event_handler.go | 379 ++++++++ .../handle_balance_request.go | 17 +- .../handle_info_request.go | 15 +- .../handle_list_transactions_request.go | 17 +- .../handle_lookup_invoice_request.go | 23 +- .../handle_make_invoice_request.go | 17 +- .../handle_multi_pay_invoice_request.go | 25 +- .../handle_multi_pay_keysend_request.go | 19 +- .../handle_pay_keysend_request.go | 23 +- .../handle_payment_request.go | 29 +- .../handle_sign_message_request.go | 17 +- nip47/models.go | 25 + nip47_notifier.go => nip47/nip47_notifier.go | 69 +- nip47/nip47_service.go | 53 + nip47/permissions.go | 82 ++ nip47/publish_nip47_info.go | 26 + screenshot_index.png | Bin 54658 -> 0 bytes screenshot_new.png | Bin 29521 -> 0 bytes service.go | 915 ------------------ service/models.go | 11 +- service/service.go | 451 +++++++++ start.go => service/start.go | 16 +- tests/create_app.go | 28 + tests/create_test_service.go | 74 ++ tests/mock_ln_client.go | 157 +++ tests/nip47_event_handler_test.go | 55 ++ .../nip47_event_handlers_test.go | 476 +-------- .../nip47_notifier_test.go | 43 +- tests/nip47_permissions_test.go | 105 ++ utils/utils.go | 27 - wails_app.go => wails/wails_app.go | 19 +- wails_handlers.go => wails/wails_handlers.go | 91 +- 48 files changed, 1894 insertions(+), 1804 deletions(-) delete mode 100644 README_GREENLIGHT.md rename backup/backup_service.go => api/backup.go (93%) rename lsp/lsp_service.go => api/lsp.go (91%) delete mode 100644 backup/models.go create mode 100644 nip47/budgets.go create mode 100644 nip47/event_handler.go rename handle_balance_request.go => nip47/handle_balance_request.go (67%) rename handle_info_request.go => nip47/handle_info_request.go (74%) rename handle_list_transactions_request.go => nip47/handle_list_transactions_request.go (74%) rename handle_lookup_invoice_request.go => nip47/handle_lookup_invoice_request.go (77%) rename handle_make_invoice_request.go => nip47/handle_make_invoice_request.go (77%) rename handle_multi_pay_invoice_request.go => nip47/handle_multi_pay_invoice_request.go (85%) rename handle_multi_pay_keysend_request.go => nip47/handle_multi_pay_keysend_request.go (83%) rename handle_pay_keysend_request.go => nip47/handle_pay_keysend_request.go (78%) rename handle_payment_request.go => nip47/handle_payment_request.go (80%) rename handle_sign_message_request.go => nip47/handle_sign_message_request.go (69%) rename nip47_notifier.go => nip47/nip47_notifier.go (62%) create mode 100644 nip47/nip47_service.go create mode 100644 nip47/permissions.go create mode 100644 nip47/publish_nip47_info.go delete mode 100644 screenshot_index.png delete mode 100644 screenshot_new.png delete mode 100644 service.go create mode 100644 service/service.go rename start.go => service/start.go (89%) create mode 100644 tests/create_app.go create mode 100644 tests/create_test_service.go create mode 100644 tests/mock_ln_client.go create mode 100644 tests/nip47_event_handler_test.go rename service_test.go => tests/nip47_event_handlers_test.go (64%) rename nip47_notifier_test.go => tests/nip47_notifier_test.go (74%) create mode 100644 tests/nip47_permissions_test.go rename wails_app.go => wails/wails_app.go (86%) rename wails_handlers.go => wails/wails_handlers.go (89%) diff --git a/README.md b/README.md index 4a55c3fb6..42cf2e6a4 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 @@ -90,7 +86,7 @@ _If you get a blank screen, try running in your normal terminal (outside of vsco ### Testing - $ go test + $ go test ./tests ### Profiling 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/api/api.go b/api/api.go index afcfb9656..0a26e6fb7 100644 --- a/api/api.go +++ b/api/api.go @@ -15,34 +15,29 @@ 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/lnclient" - "github.com/getAlby/nostr-wallet-connect/lsp" "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/getAlby/nostr-wallet-connect/service" "github.com/getAlby/nostr-wallet-connect/utils" ) type api struct { - logger *logrus.Logger - svc service.Service - lspSvc lsp.LSPService - backupSvc backup.BackupService - db *gorm.DB - dbSvc db.DBService + logger *logrus.Logger + db *gorm.DB + dbSvc db.DBService + cfg config.Config + svc service.Service } -func NewAPI(svc service.Service, logger *logrus.Logger, gormDb *gorm.DB) *api { - +func NewAPI(svc service.Service, logger *logrus.Logger, gormDb *gorm.DB, config config.Config) *api { return &api{ - svc: svc, - logger: logger, - db: gormDb, - dbSvc: db.NewDBService(gormDb, logger), - lspSvc: lsp.NewLSPService(svc, logger), - backupSvc: backup.NewBackupService(svc, logger), + logger: logger, + db: gormDb, + dbSvc: db.NewDBService(gormDb, logger), + cfg: config, + svc: svc, } } @@ -58,13 +53,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 +79,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.cfg.GetNostrPublicKey()) // if user.LightningAddress != "" { // query.Add("lud16", user.LightningAddress) // } @@ -89,7 +92,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.cfg.GetNostrPublicKey(), relayUrl, pairingSecretKey, lud16) return responseBody, nil } @@ -188,7 +191,7 @@ func (api *api) GetApp(userApp *db.App) *App { budgetUsage := int64(0) maxAmount := paySpecificPermission.MaxAmount if maxAmount > 0 { - budgetUsage = api.svc.GetBudgetUsage(&paySpecificPermission) + budgetUsage = api.svc.GetNip47Service().GetBudgetUsage(&paySpecificPermission) } response := App{ @@ -243,7 +246,7 @@ func (api *api) ListApps() ([]App, error) { apiApp.BudgetRenewal = permission.BudgetRenewal apiApp.MaxAmount = permission.MaxAmount if apiApp.MaxAmount > 0 { - apiApp.BudgetUsage = api.svc.GetBudgetUsage(&permission) + apiApp.BudgetUsage = api.svc.GetNip47Service().GetBudgetUsage(&permission) } } } @@ -289,7 +292,7 @@ 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") @@ -306,12 +309,15 @@ func (api *api) Stop() error { 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") } + return err } @@ -385,7 +391,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,7 +401,7 @@ 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") return "", err @@ -469,7 +475,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, @@ -514,13 +520,13 @@ 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() + info.OAuthRedirect = !api.cfg.GetEnv().IsDefaultClientId() albyUserIdentifier, err := api.svc.GetAlbyOAuthSvc().GetUserIdentifier() if err != nil { api.logger.WithError(err).Error("Failed to get alby user identifier") @@ -538,20 +544,20 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) { 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 } @@ -570,40 +576,40 @@ func (api *api) Setup(ctx context.Context, setupRequest *SetupRequest) error { 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) } return nil @@ -693,10 +699,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 93% rename from backup/backup_service.go rename to api/backup.go index 9bb06ed47..611373808 100644 --- a/backup/backup_service.go +++ b/api/backup.go @@ -1,4 +1,4 @@ -package backup +package api import ( "errors" @@ -12,9 +12,6 @@ import ( "path/filepath" "slices" - "github.com/getAlby/nostr-wallet-connect/service" - "github.com/sirupsen/logrus" - "crypto/aes" "crypto/cipher" "crypto/rand" @@ -23,19 +20,7 @@ import ( "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 (bs *api) CreateBackup(unlockPassword string, w io.Writer) error { var err error if !bs.svc.GetConfig().CheckUnlockPassword(unlockPassword) { @@ -146,7 +131,7 @@ func (bs *backupService) CreateBackup(unlockPassword string, w io.Writer) error return nil } -func (bs *backupService) RestoreBackup(unlockPassword string, r io.Reader) error { +func (bs *api) RestoreBackup(unlockPassword string, r io.Reader) error { workDir, err := filepath.Abs(bs.svc.GetConfig().GetEnv().Workdir) if err != nil { return fmt.Errorf("failed to get absolute workdir: %w", err) diff --git a/api/esplora.go b/api/esplora.go index f1d185501..210163d5d 100644 --- a/api/esplora.go +++ b/api/esplora.go @@ -12,7 +12,7 @@ import ( ) 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, diff --git a/lsp/lsp_service.go b/api/lsp.go similarity index 91% rename from lsp/lsp_service.go rename to api/lsp.go index fd57af0d2..f2431ad81 100644 --- a/lsp/lsp_service.go +++ b/api/lsp.go @@ -1,4 +1,4 @@ -package lsp +package api import ( "bytes" @@ -14,47 +14,35 @@ import ( "time" "github.com/getAlby/nostr-wallet-connect/lnclient" - "github.com/getAlby/nostr-wallet-connect/service" + "github.com/getAlby/nostr-wallet-connect/lsp" "github.com/sirupsen/logrus" ) -type lspService struct { - svc service.Service - logger *logrus.Logger -} - type lspConnectionInfo struct { Pubkey string Address string Port uint16 } -func NewLSPService(svc service.Service, logger *logrus.Logger) *lspService { - return &lspService{ - svc: svc, - logger: logger, - } -} - -func (ls *lspService) NewInstantChannelInvoice(ctx context.Context, request *NewInstantChannelInvoiceRequest) (*NewInstantChannelInvoiceResponse, error) { - var selectedLsp LSP +func (ls *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") } @@ -63,7 +51,7 @@ func (ls *lspService) NewInstantChannelInvoice(ctx context.Context, request *New 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") } @@ -72,12 +60,12 @@ func (ls *lspService) NewInstantChannelInvoice(ctx context.Context, request *New var lspInfo *lspConnectionInfo var err error switch selectedLsp.LspType { - case LSP_TYPE_FLOW_2_0: + case lsp.LSP_TYPE_FLOW_2_0: fallthrough - case LSP_TYPE_PMLSP: + case lsp.LSP_TYPE_PMLSP: lspInfo, err = ls.getFlowLSPInfo(selectedLsp.Url + "/info") - case LSP_TYPE_LSPS1: + case lsp.LSP_TYPE_LSPS1: lspInfo, err = ls.getLSPS1LSPInfo(selectedLsp.Url + "/get_info") default: @@ -115,11 +103,11 @@ func (ls *lspService) NewInstantChannelInvoice(ctx context.Context, request *New var fee uint64 = 0 switch selectedLsp.LspType { - case LSP_TYPE_FLOW_2_0: + case lsp.LSP_TYPE_FLOW_2_0: invoice, fee, err = ls.requestFlow20WrappedInvoice(ctx, &selectedLsp, request.Amount, nodeInfo.Pubkey) - case LSP_TYPE_PMLSP: + case lsp.LSP_TYPE_PMLSP: invoice, fee, err = ls.requestPMLSPInvoice(&selectedLsp, request.Amount, nodeInfo.Pubkey) - case LSP_TYPE_LSPS1: + case lsp.LSP_TYPE_LSPS1: invoice, fee, err = ls.requestLSPS1Invoice(ctx, &selectedLsp, request.Amount, nodeInfo.Pubkey, request.Public) default: @@ -142,7 +130,7 @@ func (ls *lspService) NewInstantChannelInvoice(ctx context.Context, request *New return newChannelResponse, nil } -func (ls *lspService) getLSPS1LSPInfo(url string) (*lspConnectionInfo, error) { +func (ls *api) getLSPS1LSPInfo(url string) (*lspConnectionInfo, error) { type LSPS1LSPInfo struct { // TODO: implement options Options interface{} `json:"options"` @@ -210,7 +198,7 @@ func (ls *lspService) getLSPS1LSPInfo(url string) (*lspConnectionInfo, error) { Port: uint16(port), }, nil } -func (ls *lspService) getFlowLSPInfo(url string) (*lspConnectionInfo, error) { +func (ls *api) getFlowLSPInfo(url string) (*lspConnectionInfo, error) { type FlowLSPConnectionMethod struct { Address string `json:"address"` Port uint16 `json:"port"` @@ -278,7 +266,7 @@ func (ls *lspService) getFlowLSPInfo(url string) (*lspConnectionInfo, error) { }, nil } -func (ls *lspService) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *LSP, amount uint64, pubkey string) (invoice string, fee uint64, err error) { +func (ls *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *lsp.LSP, amount uint64, pubkey string) (invoice string, fee uint64, err error) { ls.logger.Infoln("Requesting fee information") type FeeRequest struct { @@ -450,7 +438,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 (ls *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"` @@ -504,7 +492,12 @@ func (ls *lspService) requestPMLSPInvoice(selectedLsp *LSP, amount uint64, pubke 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 { @@ -520,7 +513,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) (invoice string, fee uint64, err error) { +func (ls *api) requestLSPS1Invoice(ctx context.Context, selectedLsp *lsp.LSP, amount uint64, pubkey string, public bool) (invoice string, fee uint64, err error) { client := http.Client{ Timeout: time.Second * 10, } diff --git a/api/models.go b/api/models.go index 1f912721f..d7663ccbc 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 { @@ -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/config/config.go b/config/config.go index 029a62494..f5130417c 100644 --- a/config/config.go +++ b/config/config.go @@ -7,12 +7,11 @@ import ( "fmt" "os" + "github.com/getAlby/nostr-wallet-connect/db" "github.com/nbd-wtf/go-nostr" "github.com/sirupsen/logrus" "gorm.io/gorm" "gorm.io/gorm/clause" - - dbModels "github.com/getAlby/nostr-wallet-connect/db" ) type config struct { @@ -107,11 +106,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 +124,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 +132,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) @@ -170,8 +169,8 @@ 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 } diff --git a/db/db_service.go b/db/db_service.go index 835ccb120..edf9bb8a5 100644 --- a/db/db_service.go +++ b/db/db_service.go @@ -3,10 +3,8 @@ package db import ( "encoding/hex" "fmt" - "strings" "time" - "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/nbd-wtf/go-nostr" "github.com/sirupsen/logrus" "gorm.io/gorm" @@ -24,7 +22,7 @@ func NewDBService(db *gorm.DB, logger *logrus.Logger) *dbService { } } -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 int, budgetRenewal string, expiresAt *time.Time, requestMethods []string) (*App, string, error) { var pairingPublicKey string var pairingSecretKey string if pubkey == "" { @@ -35,24 +33,20 @@ 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") + svc.logger.WithField("pairingPublicKey", pairingPublicKey).Error("Invalid public key format") return nil, "", fmt.Errorf("invalid public key format: %s", pairingPublicKey) } } app := App{Name: name, NostrPubkey: pairingPublicKey} - 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, @@ -71,7 +65,7 @@ func (dbSvc *dbService) CreateApp(name string, pubkey string, maxAmount int, bud }) if err != nil { - dbSvc.logger.WithError(err).Error("Failed to save app") + svc.logger.WithError(err).Error("Failed to save app") return nil, "", err } diff --git a/http/http_service.go b/http/http_service.go index 905d3dbeb..907048798 100644 --- a/http/http_service.go +++ b/http/http_service.go @@ -19,7 +19,6 @@ import ( "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/service" "github.com/getAlby/nostr-wallet-connect/api" @@ -42,7 +41,7 @@ const ( func NewHttpService(svc service.Service, logger *logrus.Logger, db *gorm.DB, eventPublisher events.EventPublisher) *HttpService { return &HttpService{ - api: api.NewAPI(svc, logger, db), + api: api.NewAPI(svc, logger, db, svc.GetConfig()), albyHttpSvc: alby.NewAlbyHttpService(svc.GetAlbyOAuthSvc(), logger, svc.GetConfig().GetEnv()), cfg: svc.GetConfig(), db: db, @@ -504,14 +503,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{ @@ -789,7 +788,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 +826,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/models.go b/lnclient/models.go index 4a5e81ed1..664f0a736 100644 --- a/lnclient/models.go +++ b/lnclient/models.go @@ -1,4 +1,3 @@ -// TODO: move to lnclient/models.go package lnclient import ( 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 index 996cd7311..efa42d246 100644 --- a/main_http.go +++ b/main_http.go @@ -16,6 +16,7 @@ import ( echologrus "github.com/davrux/echo-logrus/v4" "github.com/getAlby/nostr-wallet-connect/http" + "github.com/getAlby/nostr-wallet-connect/service" "github.com/labstack/echo/v4" log "github.com/sirupsen/logrus" ) @@ -25,28 +26,27 @@ import ( func main() { log.Info("NWC Starting in HTTP mode") ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, os.Kill) - svc, _ := NewService(ctx) + svc, _ := service.NewService(ctx) - echologrus.Logger = svc.logger + echologrus.Logger = svc.GetLogger() e := echo.New() //register shared routes - httpSvc := http.NewHttpService(svc, svc.logger, svc.db, svc.eventPublisher) + httpSvc := http.NewHttpService(svc, svc.GetLogger(), svc.GetDB(), svc.GetEventPublisher()) 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) + if err := e.Start(fmt.Sprintf(":%v", svc.GetConfig().GetEnv().Port)); err != nil && err != nethttp.ErrServerClosed { + svc.GetLogger().Fatalf("shutting down the server: %v", err) } }() //handle graceful shutdown <-ctx.Done() - svc.logger.Infof("Shutting down echo server...") + svc.GetLogger().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") + svc.GetLogger().Info("Echo server exited") + svc.WaitShutdown() + svc.GetLogger().Info("Service exited") } diff --git a/main_wails.go b/main_wails.go index 4ae8dec59..7e4d6ed78 100644 --- a/main_wails.go +++ b/main_wails.go @@ -5,25 +5,33 @@ package main import ( "context" + "embed" + "github.com/getAlby/nostr-wallet-connect/service" + "github.com/getAlby/nostr-wallet-connect/wails" log "github.com/sirupsen/logrus" ) +//go:embed all:frontend/dist +var assets embed.FS + +//go:embed appicon.png +var appIcon []byte + // ignore this warning: we use build tags // this function will only be executed if the wails tag is set 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) + svc.GetLogger().Info("Wails app exited") - svc.logger.Info("Cancelling service context...") + svc.GetLogger().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() + svc.GetLogger().Info("Service exited") } diff --git a/nip47/budgets.go b/nip47/budgets.go new file mode 100644 index 000000000..4fd2ddf7c --- /dev/null +++ b/nip47/budgets.go @@ -0,0 +1,40 @@ +package nip47 + +import ( + "time" + + "github.com/getAlby/nostr-wallet-connect/db" +) + +func (svc *nip47Service) GetStartOfBudget(budget_type string, createdAt time.Time) time.Time { + now := time.Now() + switch budget_type { + case 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 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 BUDGET_RENEWAL_MONTHLY: + return time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) + case BUDGET_RENEWAL_YEARLY: + return time.Date(now.Year(), time.January, 1, 0, 0, 0, 0, now.Location()) + default: //"never" + return createdAt + } +} + +func (svc *nip47Service) 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, svc.GetStartOfBudget(appPermission.BudgetRenewal, appPermission.App.CreatedAt)).Scan(&result) + return int64(result.Sum) +} diff --git a/nip47/event_handler.go b/nip47/event_handler.go new file mode 100644 index 000000000..192bea65d --- /dev/null +++ b/nip47/event_handler.go @@ -0,0 +1,379 @@ +package nip47 + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "slices" + "time" + + "github.com/getAlby/nostr-wallet-connect/db" + "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) { + var nip47Response *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 = &Response{ + Error: &Error{ + Code: 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.PublishResponseEvent(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 = &Response{ + Error: &Error{ + Code: 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.PublishResponseEvent(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 = &Response{ + Error: &Error{ + Code: 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.PublishResponseEvent(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 := &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.PublishResponseEvent to also use the channel + publishResponse := func(nip47Response *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.PublishResponseEvent(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 MULTI_PAY_INVOICE_METHOD: + svc.HandleMultiPayInvoiceEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) + case MULTI_PAY_KEYSEND_METHOD: + svc.HandleMultiPayKeysendEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) + case PAY_INVOICE_METHOD: + svc.HandlePayInvoiceEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) + case PAY_KEYSEND_METHOD: + svc.HandlePayKeysendEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) + case GET_BALANCE_METHOD: + svc.HandleGetBalanceEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) + case MAKE_INVOICE_METHOD: + svc.HandleMakeInvoiceEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) + case LOOKUP_INVOICE_METHOD: + svc.HandleLookupInvoiceEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) + case LIST_TRANSACTIONS_METHOD: + svc.HandleListTransactionsEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) + case GET_INFO_METHOD: + svc.HandleGetInfoEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) + case SIGN_MESSAGE_METHOD: + svc.HandleSignMessageEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) + default: + svc.HandleUnknownMethod(ctx, nip47Request, publishResponse) + } +} + +func (svc *nip47Service) HandleUnknownMethod(ctx context.Context, nip47Request *Request, publishResponse func(*Response, nostr.Tags)) { + publishResponse(&Response{ + ResultType: nip47Request.Method, + Error: &Error{ + Code: 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.cfg.GetNostrPublicKey(), + CreatedAt: nostr.Now(), + Kind: RESPONSE_KIND, + Tags: allTags, + Content: msg, + } + err = resp.Sign(svc.cfg.GetNostrSecretKey()) + if err != nil { + return nil, err + } + return resp, nil +} + +func (svc *nip47Service) 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, PAY_INVOICE_METHOD) { + // all payment methods are tied to the pay_invoice permission + requestMethods = append(requestMethods, PAY_KEYSEND_METHOD, MULTI_PAY_INVOICE_METHOD, MULTI_PAY_KEYSEND_METHOD) + } + + return requestMethods +} + +func (svc *nip47Service) decodeNip47Request(nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, methodParams interface{}) *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 &Response{ + ResultType: nip47Request.Method, + Error: &Error{ + Code: ERROR_BAD_REQUEST, + Message: err.Error(), + }} + } + return 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 { + 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 +} diff --git a/handle_balance_request.go b/nip47/handle_balance_request.go similarity index 67% rename from handle_balance_request.go rename to nip47/handle_balance_request.go index dd85d33f9..17ff24820 100644 --- a/handle_balance_request.go +++ b/nip47/handle_balance_request.go @@ -1,10 +1,9 @@ -package main +package nip47 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" ) @@ -13,7 +12,7 @@ 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)) { +func (svc *nip47Service) HandleGetBalanceEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) { resp := svc.checkPermission(nip47Request, requestEvent.NostrId, app, 0) if resp != nil { @@ -32,22 +31,22 @@ func (svc *Service) HandleGetBalanceEvent(ctx context.Context, nip47Request *nip "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, }).Infof("Failed to fetch balance: %v", err) - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, + Error: &Error{ + Code: ERROR_INTERNAL, Message: err.Error(), }, }, nostr.Tags{}) return } - responsePayload := &nip47.BalanceResponse{ + responsePayload := &BalanceResponse{ Balance: balance, } appPermission := db.AppPermission{} - svc.db.Where("app_id = ? AND request_method = ?", app.ID, nip47.PAY_INVOICE_METHOD).First(&appPermission) + svc.db.Where("app_id = ? AND request_method = ?", app.ID, PAY_INVOICE_METHOD).First(&appPermission) maxAmount := appPermission.MaxAmount if maxAmount > 0 { @@ -55,7 +54,7 @@ func (svc *Service) HandleGetBalanceEvent(ctx context.Context, nip47Request *nip responsePayload.BudgetRenewal = appPermission.BudgetRenewal } - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, Result: responsePayload, }, nostr.Tags{}) diff --git a/handle_info_request.go b/nip47/handle_info_request.go similarity index 74% rename from handle_info_request.go rename to nip47/handle_info_request.go index f2f610ed7..4fe999db7 100644 --- a/handle_info_request.go +++ b/nip47/handle_info_request.go @@ -1,15 +1,14 @@ -package main +package nip47 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)) { +func (svc *nip47Service) HandleGetInfoEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) { resp := svc.checkPermission(nip47Request, requestEvent.NostrId, app, 0) if resp != nil { @@ -29,10 +28,10 @@ func (svc *Service) HandleGetInfoEvent(ctx context.Context, nip47Request *nip47. "appId": app.ID, }).Infof("Failed to fetch node info: %v", err) - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, + Error: &Error{ + Code: ERROR_INTERNAL, Message: err.Error(), }, }, nostr.Tags{}) @@ -45,7 +44,7 @@ func (svc *Service) HandleGetInfoEvent(ctx context.Context, nip47Request *nip47. network = "mainnet" } - responsePayload := &nip47.GetInfoResponse{ + responsePayload := &GetInfoResponse{ Alias: info.Alias, Color: info.Color, Pubkey: info.Pubkey, @@ -55,7 +54,7 @@ func (svc *Service) HandleGetInfoEvent(ctx context.Context, nip47Request *nip47. Methods: svc.GetMethods(app), } - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, Result: responsePayload, }, nostr.Tags{}) diff --git a/handle_list_transactions_request.go b/nip47/handle_list_transactions_request.go similarity index 74% rename from handle_list_transactions_request.go rename to nip47/handle_list_transactions_request.go index fa883752f..305fbd73d 100644 --- a/handle_list_transactions_request.go +++ b/nip47/handle_list_transactions_request.go @@ -1,17 +1,16 @@ -package main +package nip47 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)) { +func (svc *nip47Service) HandleListTransactionsEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) { - listParams := &nip47.ListTransactionsParams{} + listParams := &ListTransactionsParams{} resp := svc.decodeNip47Request(nip47Request, requestEvent, app, listParams) if resp != nil { publishResponse(resp, nostr.Tags{}) @@ -44,21 +43,21 @@ func (svc *Service) HandleListTransactionsEvent(ctx context.Context, nip47Reques "appId": app.ID, }).Infof("Failed to fetch transactions: %v", err) - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, + Error: &Error{ + Code: ERROR_INTERNAL, Message: err.Error(), }, }, nostr.Tags{}) return } - responsePayload := &nip47.ListTransactionsResponse{ + responsePayload := &ListTransactionsResponse{ Transactions: transactions, } - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, Result: responsePayload, }, nostr.Tags{}) diff --git a/handle_lookup_invoice_request.go b/nip47/handle_lookup_invoice_request.go similarity index 77% rename from handle_lookup_invoice_request.go rename to nip47/handle_lookup_invoice_request.go index 352fa8626..38e354de9 100644 --- a/handle_lookup_invoice_request.go +++ b/nip47/handle_lookup_invoice_request.go @@ -1,4 +1,4 @@ -package main +package nip47 import ( "context" @@ -6,15 +6,14 @@ import ( "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)) { +func (svc *nip47Service) HandleLookupInvoiceEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) { - lookupInvoiceParams := &nip47.LookupInvoiceParams{} + lookupInvoiceParams := &LookupInvoiceParams{} resp := svc.decodeNip47Request(nip47Request, requestEvent, app, lookupInvoiceParams) if resp != nil { publishResponse(resp, nostr.Tags{}) @@ -45,10 +44,10 @@ func (svc *Service) HandleLookupInvoiceEvent(ctx context.Context, nip47Request * "invoice": lookupInvoiceParams.Invoice, }).Errorf("Failed to decode bolt11 invoice: %v", err) - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, + Error: &Error{ + Code: ERROR_INTERNAL, Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()), }, }, nostr.Tags{}) @@ -66,21 +65,21 @@ func (svc *Service) HandleLookupInvoiceEvent(ctx context.Context, nip47Request * "paymentHash": lookupInvoiceParams.PaymentHash, }).Infof("Failed to lookup invoice: %v", err) - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, + Error: &Error{ + Code: ERROR_INTERNAL, Message: err.Error(), }, }, nostr.Tags{}) return } - responsePayload := &nip47.LookupInvoiceResponse{ + responsePayload := &LookupInvoiceResponse{ Transaction: *transaction, } - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, Result: responsePayload, }, nostr.Tags{}) diff --git a/handle_make_invoice_request.go b/nip47/handle_make_invoice_request.go similarity index 77% rename from handle_make_invoice_request.go rename to nip47/handle_make_invoice_request.go index 8666df1b6..c61b04723 100644 --- a/handle_make_invoice_request.go +++ b/nip47/handle_make_invoice_request.go @@ -1,17 +1,16 @@ -package main +package nip47 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)) { +func (svc *nip47Service) HandleMakeInvoiceEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) { - makeInvoiceParams := &nip47.MakeInvoiceParams{} + makeInvoiceParams := &MakeInvoiceParams{} resp := svc.decodeNip47Request(nip47Request, requestEvent, app, makeInvoiceParams) if resp != nil { publishResponse(resp, nostr.Tags{}) @@ -49,21 +48,21 @@ func (svc *Service) HandleMakeInvoiceEvent(ctx context.Context, nip47Request *ni "expiry": makeInvoiceParams.Expiry, }).Infof("Failed to make invoice: %v", err) - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, + Error: &Error{ + Code: ERROR_INTERNAL, Message: err.Error(), }, }, nostr.Tags{}) return } - responsePayload := &nip47.MakeInvoiceResponse{ + responsePayload := &MakeInvoiceResponse{ Transaction: *transaction, } - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, Result: responsePayload, }, nostr.Tags{}) diff --git a/handle_multi_pay_invoice_request.go b/nip47/handle_multi_pay_invoice_request.go similarity index 85% rename from handle_multi_pay_invoice_request.go rename to nip47/handle_multi_pay_invoice_request.go index 7fd6e28da..fa09658ec 100644 --- a/handle_multi_pay_invoice_request.go +++ b/nip47/handle_multi_pay_invoice_request.go @@ -1,4 +1,4 @@ -package main +package nip47 import ( "context" @@ -8,16 +8,15 @@ import ( "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)) { +func (svc *nip47Service) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) { - multiPayParams := &nip47.MultiPayInvoiceParams{} + multiPayParams := &MultiPayInvoiceParams{} resp := svc.decodeNip47Request(nip47Request, requestEvent, app, multiPayParams) if resp != nil { publishResponse(resp, nostr.Tags{}) @@ -29,7 +28,7 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Request for _, invoiceInfo := range multiPayParams.Invoices { wg.Add(1) // TODO: we should call the handle_payment_request (most of this code is duplicated) - go func(invoiceInfo nip47.MultiPayInvoiceElement) { + go func(invoiceInfo MultiPayInvoiceElement) { defer wg.Done() bolt11 := invoiceInfo.Invoice // Convert invoice to lowercase string @@ -44,10 +43,10 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Request // TODO: Decide what to do if id is empty dTag := []string{"d", invoiceInfo.Id} - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, + Error: &Error{ + Code: ERROR_INTERNAL, Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()), }, }, nostr.Tags{dTag}) @@ -103,10 +102,10 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Request }, }) - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, + Error: &Error{ + Code: ERROR_INTERNAL, Message: err.Error(), }, }, nostr.Tags{dTag}) @@ -125,9 +124,9 @@ func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Request "amount": paymentRequest.MSatoshi / 1000, }, }) - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, - Result: nip47.PayResponse{ + Result: PayResponse{ Preimage: response.Preimage, FeesPaid: response.Fee, }, diff --git a/handle_multi_pay_keysend_request.go b/nip47/handle_multi_pay_keysend_request.go similarity index 83% rename from handle_multi_pay_keysend_request.go rename to nip47/handle_multi_pay_keysend_request.go index b1acd07a6..2dac7e30f 100644 --- a/handle_multi_pay_keysend_request.go +++ b/nip47/handle_multi_pay_keysend_request.go @@ -1,4 +1,4 @@ -package main +package nip47 import ( "context" @@ -6,14 +6,13 @@ import ( "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)) { +func (svc *nip47Service) HandleMultiPayKeysendEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) { - multiPayParams := &nip47.MultiPayKeysendParams{} + multiPayParams := &MultiPayKeysendParams{} resp := svc.decodeNip47Request(nip47Request, requestEvent, app, multiPayParams) if resp != nil { publishResponse(resp, nostr.Tags{}) @@ -24,7 +23,7 @@ func (svc *Service) HandleMultiPayKeysendEvent(ctx context.Context, nip47Request var mu sync.Mutex for _, keysendInfo := range multiPayParams.Keysends { wg.Add(1) - go func(keysendInfo nip47.MultiPayKeysendElement) { + go func(keysendInfo MultiPayKeysendElement) { defer wg.Done() keysendDTagValue := keysendInfo.Id @@ -75,10 +74,10 @@ func (svc *Service) HandleMultiPayKeysendEvent(ctx context.Context, nip47Request }, }) - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, + Error: &Error{ + Code: ERROR_INTERNAL, Message: err.Error(), }, }, nostr.Tags{dTag}) @@ -96,9 +95,9 @@ func (svc *Service) HandleMultiPayKeysendEvent(ctx context.Context, nip47Request "amount": keysendInfo.Amount / 1000, }, }) - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, - Result: nip47.PayResponse{ + Result: PayResponse{ Preimage: preimage, }, }, nostr.Tags{dTag}) diff --git a/handle_pay_keysend_request.go b/nip47/handle_pay_keysend_request.go similarity index 78% rename from handle_pay_keysend_request.go rename to nip47/handle_pay_keysend_request.go index 97c867a4d..52d7e3f0c 100644 --- a/handle_pay_keysend_request.go +++ b/nip47/handle_pay_keysend_request.go @@ -1,18 +1,17 @@ -package main +package nip47 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)) { +func (svc *nip47Service) HandlePayKeysendEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) { - payParams := &nip47.KeysendParams{} + payParams := &KeysendParams{} resp := svc.decodeNip47Request(nip47Request, requestEvent, app, payParams) if resp != nil { publishResponse(resp, nostr.Tags{}) @@ -28,10 +27,10 @@ func (svc *Service) HandlePayKeysendEvent(ctx context.Context, nip47Request *nip payment := db.Payment{App: *app, RequestEvent: *requestEvent, Amount: uint(payParams.Amount / 1000)} err := svc.db.Create(&payment).Error if err != nil { - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, + Error: &Error{ + Code: ERROR_INTERNAL, Message: err.Error(), }, }, nostr.Tags{}) @@ -59,10 +58,10 @@ func (svc *Service) HandlePayKeysendEvent(ctx context.Context, nip47Request *nip "amount": payParams.Amount / 1000, }, }) - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, + Error: &Error{ + Code: ERROR_INTERNAL, Message: err.Error(), }, }, nostr.Tags{}) @@ -77,9 +76,9 @@ func (svc *Service) HandlePayKeysendEvent(ctx context.Context, nip47Request *nip "amount": payParams.Amount / 1000, }, }) - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, - Result: nip47.PayResponse{ + Result: PayResponse{ Preimage: preimage, }, }, nostr.Tags{}) diff --git a/handle_payment_request.go b/nip47/handle_payment_request.go similarity index 80% rename from handle_payment_request.go rename to nip47/handle_payment_request.go index 16afba69f..71f381cf0 100644 --- a/handle_payment_request.go +++ b/nip47/handle_payment_request.go @@ -1,4 +1,4 @@ -package main +package nip47 import ( "context" @@ -7,15 +7,14 @@ import ( "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)) { +func (svc *nip47Service) HandlePayInvoiceEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) { - payParams := &nip47.PayParams{} + payParams := &PayParams{} resp := svc.decodeNip47Request(nip47Request, requestEvent, app, payParams) if resp != nil { publishResponse(resp, nostr.Tags{}) @@ -33,10 +32,10 @@ func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, nip47Request *nip "bolt11": bolt11, }).Errorf("Failed to decode bolt11 invoice: %v", err) - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, + Error: &Error{ + Code: ERROR_INTERNAL, Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()), }, }, nostr.Tags{}) @@ -52,10 +51,10 @@ func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, nip47Request *nip 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{ + publishResponse(&Response{ ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, + Error: &Error{ + Code: ERROR_INTERNAL, Message: err.Error(), }, }, nostr.Tags{}) @@ -83,10 +82,10 @@ func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, nip47Request *nip "amount": paymentRequest.MSatoshi / 1000, }, }) - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, + Error: &Error{ + Code: ERROR_INTERNAL, Message: err.Error(), }, }, nostr.Tags{}) @@ -104,9 +103,9 @@ func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, nip47Request *nip }, }) - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, - Result: nip47.PayResponse{ + Result: PayResponse{ Preimage: response.Preimage, FeesPaid: response.Fee, }, diff --git a/handle_sign_message_request.go b/nip47/handle_sign_message_request.go similarity index 69% rename from handle_sign_message_request.go rename to nip47/handle_sign_message_request.go index bf4e22eb7..aadb0c348 100644 --- a/handle_sign_message_request.go +++ b/nip47/handle_sign_message_request.go @@ -1,16 +1,15 @@ -package main +package nip47 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{} +func (svc *nip47Service) HandleSignMessageEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) { + signParams := &SignMessageParams{} resp := svc.decodeNip47Request(nip47Request, requestEvent, app, signParams) if resp != nil { publishResponse(resp, nostr.Tags{}) @@ -34,22 +33,22 @@ func (svc *Service) HandleSignMessageEvent(ctx context.Context, nip47Request *ni "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, }).Infof("Failed to sign message: %v", err) - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, - Error: &nip47.Error{ - Code: nip47.ERROR_INTERNAL, + Error: &Error{ + Code: ERROR_INTERNAL, Message: err.Error(), }, }, nostr.Tags{}) return } - responsePayload := nip47.SignMessageResponse{ + responsePayload := SignMessageResponse{ Message: signParams.Message, Signature: signature, } - publishResponse(&nip47.Response{ + publishResponse(&Response{ ResultType: nip47Request.Method, Result: responsePayload, }, nostr.Tags{}) diff --git a/nip47/models.go b/nip47/models.go index d2cabf98e..d97da364a 100644 --- a/nip47/models.go +++ b/nip47/models.go @@ -1,9 +1,12 @@ package nip47 import ( + "context" "encoding/json" + "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/nbd-wtf/go-nostr" ) const ( @@ -171,3 +174,25 @@ type SignMessageResponse struct { Message string `json:"message"` Signature string `json:"signature"` } + +type Nip47Service interface { + HasPermission(app *db.App, requestMethod string, amount int64) (result bool, code string, message string) + GetBudgetUsage(appPermission *db.AppPermission) int64 + StartNotifier(ctx context.Context, relay *nostr.Relay, lnClient lnclient.LNClient) + HandleEvent(ctx context.Context, sub *nostr.Subscription, event *nostr.Event) + PublishNip47Info(ctx context.Context, relay *nostr.Relay) error + Stop() // TODO: remove and replace with context + + CreateResponse(initialEvent *nostr.Event, content interface{}, tags nostr.Tags, ss []byte) (result *nostr.Event, err error) + HandleUnknownMethod(ctx context.Context, nip47Request *Request, publishResponse func(*Response, nostr.Tags)) + HandleGetBalanceEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) + HandleGetInfoEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) + HandleListTransactionsEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) + HandleLookupInvoiceEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) + HandleMakeInvoiceEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) + HandleMultiPayInvoiceEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) + HandleMultiPayKeysendEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) + HandlePayKeysendEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) + HandlePayInvoiceEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) + HandleSignMessageEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) +} diff --git a/nip47_notifier.go b/nip47/nip47_notifier.go similarity index 62% rename from nip47_notifier.go rename to nip47/nip47_notifier.go index 98ffc1d6e..f2113a6ff 100644 --- a/nip47_notifier.go +++ b/nip47/nip47_notifier.go @@ -1,17 +1,18 @@ -// TODO: move to nip47 -package main +package nip47 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/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip04" "github.com/sirupsen/logrus" + "gorm.io/gorm" ) type Relay interface { @@ -19,14 +20,22 @@ type Relay interface { } type Nip47Notifier struct { - svc *Service - relay Relay + db *gorm.DB + nip47Svc *nip47Service + relay Relay + logger *logrus.Logger + cfg config.Config + lnClient lnclient.LNClient } -func NewNip47Notifier(svc *Service, relay Relay) *Nip47Notifier { +func NewNip47Notifier(nip47Svc *nip47Service, relay Relay, logger *logrus.Logger, cfg config.Config, db *gorm.DB, lnClient lnclient.LNClient) *Nip47Notifier { return &Nip47Notifier{ - svc: svc, - relay: relay, + nip47Svc: nip47Svc, + relay: relay, + logger: logger, + cfg: cfg, + db: db, + lnClient: lnClient, } } @@ -35,40 +44,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") + notifier.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. + notifier.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.nip47Svc.HasPermission(&app, NOTIFICATIONS_PERMISSION, 0) if !hasPermission { continue } @@ -76,15 +81,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) { + notifier.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.cfg.GetNostrSecretKey()) if err != nil { - notifier.svc.logger.WithFields(logrus.Fields{ + notifier.logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).WithError(err).Error("Failed to compute shared secret") @@ -93,7 +98,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{ + notifier.logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).WithError(err).Error("Failed to stringify notification") @@ -101,7 +106,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{ + notifier.logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).WithError(err).Error("Failed to encrypt notification payload") @@ -112,15 +117,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.cfg.GetNostrPublicKey(), CreatedAt: nostr.Now(), - Kind: nip47.NOTIFICATION_KIND, + Kind: NOTIFICATION_KIND, Tags: allTags, Content: msg, } - err = event.Sign(notifier.svc.cfg.GetNostrSecretKey()) + err = event.Sign(notifier.cfg.GetNostrSecretKey()) if err != nil { - notifier.svc.logger.WithFields(logrus.Fields{ + notifier.logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).WithError(err).Error("Failed to sign event") @@ -129,13 +134,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{ + notifier.logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).WithError(err).Error("Failed to publish notification") return } - notifier.svc.logger.WithFields(logrus.Fields{ + notifier.logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).Info("Published notification event") diff --git a/nip47/nip47_service.go b/nip47/nip47_service.go new file mode 100644 index 000000000..0309bd21f --- /dev/null +++ b/nip47/nip47_service.go @@ -0,0 +1,53 @@ +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/nbd-wtf/go-nostr" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type nip47Service struct { + db *gorm.DB + logger *logrus.Logger + nip47NotificationQueue Nip47NotificationQueue + eventPublisher events.EventPublisher + cfg config.Config + lnClient lnclient.LNClient +} + +func NewNip47Service(db *gorm.DB, logger *logrus.Logger, eventPublisher events.EventPublisher, cfg config.Config, lnClient lnclient.LNClient) *nip47Service { + nip47NotificationQueue := NewNip47NotificationQueue(logger) + eventPublisher.RegisterSubscriber(nip47NotificationQueue) + return &nip47Service{ + nip47NotificationQueue: nip47NotificationQueue, + db: db, + logger: logger, + eventPublisher: eventPublisher, + cfg: cfg, + lnClient: lnClient, + } +} + +func (svc *nip47Service) Stop() { + svc.eventPublisher.RemoveSubscriber(svc.nip47NotificationQueue) +} + +func (svc *nip47Service) StartNotifier(ctx context.Context, relay *nostr.Relay, lnClient lnclient.LNClient) { + nip47Notifier := NewNip47Notifier(svc, relay, svc.logger, svc.cfg, svc.db, lnClient) + go func() { + for { + select { + case <-ctx.Done(): + // subscription ended + return + case event := <-svc.nip47NotificationQueue.Channel(): + nip47Notifier.ConsumeEvent(ctx, event) + } + } + }() +} diff --git a/nip47/permissions.go b/nip47/permissions.go new file mode 100644 index 000000000..8b7c68934 --- /dev/null +++ b/nip47/permissions.go @@ -0,0 +1,82 @@ +package nip47 + +import ( + "fmt" + "time" + + "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/events" + "github.com/sirupsen/logrus" +) + +func (svc *nip47Service) HasPermission(app *db.App, requestMethod string, amount int64) (result bool, code string, message string) { + switch requestMethod { + case PAY_INVOICE_METHOD, PAY_KEYSEND_METHOD, MULTI_PAY_INVOICE_METHOD, MULTI_PAY_KEYSEND_METHOD: + requestMethod = 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, 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, ERROR_EXPIRED, "This app has expired" + } + + if requestMethod == PAY_INVOICE_METHOD { + maxAmount := appPermission.MaxAmount + if maxAmount != 0 { + budgetUsage := svc.GetBudgetUsage(&appPermission) + + if budgetUsage+amount/1000 > int64(maxAmount) { + return false, ERROR_QUOTA_EXCEEDED, "Insufficient budget remaining to make payment" + } + } + } + return true, "", "" +} + +func (svc *nip47Service) checkPermission(nip47Request *Request, requestNostrEventId string, app *db.App, amount int64) *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 &Response{ + ResultType: nip47Request.Method, + Error: &Error{ + Code: code, + Message: message, + }, + } + } + return nil +} diff --git a/nip47/publish_nip47_info.go b/nip47/publish_nip47_info.go new file mode 100644 index 000000000..dc18e1f55 --- /dev/null +++ b/nip47/publish_nip47_info.go @@ -0,0 +1,26 @@ +package nip47 + +import ( + "context" + "fmt" + + "github.com/nbd-wtf/go-nostr" +) + +func (svc *nip47Service) PublishNip47Info(ctx context.Context, relay *nostr.Relay) error { + ev := &nostr.Event{} + ev.Kind = INFO_EVENT_KIND + ev.Content = CAPABILITIES + ev.CreatedAt = nostr.Now() + ev.PubKey = svc.cfg.GetNostrPublicKey() + ev.Tags = nostr.Tags{[]string{"notifications", 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 +} diff --git a/screenshot_index.png b/screenshot_index.png deleted file mode 100644 index 519300dc922f14d22a988ad7224017bbcde53c3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54658 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 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/models.go b/service/models.go index 59ecbf9dd..298bacde9 100644 --- a/service/models.go +++ b/service/models.go @@ -3,8 +3,11 @@ 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/nip47" + "github.com/sirupsen/logrus" + "gorm.io/gorm" ) type Service interface { @@ -14,7 +17,11 @@ type Service interface { StopApp() StopLNClient() error StopDb() error - GetBudgetUsage(appPermission *db.AppPermission) int64 GetLogFilePath() string GetAlbyOAuthSvc() alby.AlbyOAuthService + GetNip47Service() nip47.Nip47Service + GetLogger() *logrus.Logger + GetDB() *gorm.DB + WaitShutdown() + GetEventPublisher() events.EventPublisher } diff --git a/service/service.go b/service/service.go new file mode 100644 index 000000000..218b2c4bd --- /dev/null +++ b/service/service.go @@ -0,0 +1,451 @@ +package service + +import ( + "context" + "database/sql" + "errors" + "fmt" + "net/http" + "net/http/pprof" + "os" + "path" + "path/filepath" + "strconv" + "strings" + "sync" + + "github.com/adrg/xdg" + "github.com/nbd-wtf/go-nostr" + "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/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" +) + +type service struct { + cfg config.Config + db *gorm.DB + lnClient lnclient.LNClient + logger *logrus.Logger + albyOAuthSvc alby.AlbyOAuthService + eventPublisher events.EventPublisher + ctx context.Context + wg *sync.WaitGroup + nip47Service nip47.Nip47Service + appCancelFn context.CancelFunc +} + +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 + } + + var wg sync.WaitGroup + svc := &service{ + cfg: cfg, + db: gormDB, + ctx: ctx, + wg: &wg, + logger: logger, + eventPublisher: eventPublisher, + 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", + }) + // TODO: move this, use contexts instead + svc.nip47Service.Stop() + } + 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 { + svc.nip47Service.StartNotifier(ctx, sub.Relay, svc.lnClient) + + 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.nip47Service.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) 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 +} + +func (svc *service) GetNip47Service() nip47.Nip47Service { + return svc.nip47Service +} + +func (svc *service) GetDB() *gorm.DB { + return svc.db +} + +func (svc *service) GetLogger() *logrus.Logger { + return svc.logger +} + +func (svc *service) GetEventPublisher() events.EventPublisher { + return svc.eventPublisher +} + +func (svc *service) WaitShutdown() { + svc.logger.Info("Waiting for service to exit...") + svc.wg.Wait() +} diff --git a/start.go b/service/start.go similarity index 89% rename from start.go rename to service/start.go index 90a17f3c1..6d7479b6a 100644 --- a/start.go +++ b/service/start.go @@ -1,4 +1,4 @@ -package main +package service import ( "context" @@ -10,9 +10,13 @@ import ( "github.com/sirupsen/logrus" "github.com/getAlby/nostr-wallet-connect/events" + "github.com/getAlby/nostr-wallet-connect/nip47" ) -func (svc *Service) StartNostr(ctx context.Context, encryptionKey string) error { +func (svc *service) StartNostr(ctx context.Context, encryptionKey string) error { + + svc.nip47Service = nip47.NewNip47Service(svc.db, svc.logger, svc.eventPublisher, svc.cfg, svc.lnClient) + relayUrl := svc.cfg.GetRelayUrl() err := svc.cfg.Start(encryptionKey) @@ -68,7 +72,7 @@ func (svc *Service) StartNostr(ctx context.Context, encryptionKey string) error } //publish event with NIP-47 info - err = svc.PublishNip47Info(ctx, relay) + err = svc.nip47Service.PublishNip47Info(ctx, relay) if err != nil { svc.logger.WithError(err).Error("Could not publish NIP47 info") } @@ -102,7 +106,7 @@ func (svc *Service) StartNostr(ctx context.Context, encryptionKey string) error return nil } -func (svc *Service) StartApp(encryptionKey string) error { +func (svc *service) StartApp(encryptionKey string) error { if !svc.cfg.CheckUnlockPassword(encryptionKey) { svc.logger.Errorf("Invalid password") return errors.New("invalid password") @@ -126,14 +130,14 @@ func (svc *Service) StartApp(encryptionKey string) error { } // TODO: remove and call StopLNClient() instead -func (svc *Service) StopApp() { +func (svc *service) StopApp() { if svc.appCancelFn != nil { svc.appCancelFn() svc.wg.Wait() } } -func (svc *Service) Shutdown() { +func (svc *service) Shutdown() { svc.StopLNClient() svc.eventPublisher.Publish(&events.Event{ Event: "nwc_stopped", diff --git a/tests/create_app.go b/tests/create_app.go new file mode 100644 index 000000000..f904c5901 --- /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.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 +} diff --git a/tests/create_test_service.go b/tests/create_test_service.go new file mode 100644 index 000000000..f403bc038 --- /dev/null +++ b/tests/create_test_service.go @@ -0,0 +1,74 @@ +package tests + +import ( + "os" + + "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/migrations" + "github.com/getAlby/nostr-wallet-connect/nip47" + "github.com/glebarez/sqlite" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +const testDB = "test.db" + +func createTestService() (svc *TestService, err error) { + gormDb, err := gorm.Open(sqlite.Open(testDB), &gorm.Config{}) + if err != nil { + return nil, err + } + + mockLn, err := NewMockLn() + 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("") + + eventPublisher := events.NewEventPublisher(logger) + + return &TestService{ + cfg: cfg, + db: gormDb, + lnClient: mockLn, + logger: logger, + eventPublisher: eventPublisher, + nip47Svc: nip47.NewNip47Service(gormDb, logger, eventPublisher, cfg, mockLn), + }, nil +} + +type TestService struct { + cfg config.Config + db *gorm.DB + lnClient lnclient.LNClient + logger *logrus.Logger + eventPublisher events.EventPublisher + nip47Svc nip47.Nip47Service +} + +func removeTestService() { + os.Remove(testDB) +} diff --git a/tests/mock_ln_client.go b/tests/mock_ln_client.go new file mode 100644 index 000000000..086bb0411 --- /dev/null +++ b/tests/mock_ln_client.go @@ -0,0 +1,157 @@ +package tests + +import ( + "context" + "time" + + "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/nip47" +) + +// 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] + +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/tests/nip47_event_handler_test.go b/tests/nip47_event_handler_test.go new file mode 100644 index 000000000..da723cf77 --- /dev/null +++ b/tests/nip47_event_handler_test.go @@ -0,0 +1,55 @@ +package tests + +import ( + "encoding/json" + "testing" + + "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 TestCreateResponse(t *testing.T) { + defer removeTestService() + svc, err := createTestService() + 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.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.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)) +} diff --git a/service_test.go b/tests/nip47_event_handlers_test.go similarity index 64% rename from service_test.go rename to tests/nip47_event_handlers_test.go index fd41d8554..197dee76e 100644 --- a/service_test.go +++ b/tests/nip47_event_handlers_test.go @@ -1,28 +1,20 @@ -package main +package tests 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" +// TODO: split up this file! const nip47GetBalanceJson = ` { @@ -206,217 +198,18 @@ const nip47PayJsonNoInvoice = ` } ` -// 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) + defer removeTestService() + svc, err := createTestService() assert.NoError(t, err) app, ss, err := createApp(svc) assert.NoError(t, err) @@ -447,7 +240,7 @@ func TestHandleMultiPayInvoiceEvent(t *testing.T) { dTags = append(dTags, tags) } - svc.HandleMultiPayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.HandleMultiPayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, 2, len(responses)) assert.Equal(t, 2, len(dTags)) @@ -475,7 +268,7 @@ func TestHandleMultiPayInvoiceEvent(t *testing.T) { requestEvent.NostrId = reqEvent.ID responses = []*nip47.Response{} dTags = []nostr.Tags{} - svc.HandleMultiPayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.HandleMultiPayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, 2, len(responses)) for i := 0; i < len(responses); i++ { @@ -495,7 +288,7 @@ func TestHandleMultiPayInvoiceEvent(t *testing.T) { requestEvent.NostrId = reqEvent.ID responses = []*nip47.Response{} dTags = []nostr.Tags{} - svc.HandleMultiPayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.HandleMultiPayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, 2, len(responses)) assert.Equal(t, "invoiceId123", dTags[0].GetFirst([]string{"d"}).Value()) @@ -522,7 +315,7 @@ func TestHandleMultiPayInvoiceEvent(t *testing.T) { requestEvent.NostrId = reqEvent.ID responses = []*nip47.Response{} dTags = []nostr.Tags{} - svc.HandleMultiPayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.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 @@ -536,10 +329,8 @@ func TestHandleMultiPayInvoiceEvent(t *testing.T) { func TestHandleMultiPayKeysendEvent(t *testing.T) { ctx := context.TODO() - defer os.Remove(testDB) - mockLn, err := NewMockLn() - assert.NoError(t, err) - svc, err := createTestService(mockLn) + defer removeTestService() + svc, err := createTestService() assert.NoError(t, err) app, ss, err := createApp(svc) assert.NoError(t, err) @@ -570,7 +361,7 @@ func TestHandleMultiPayKeysendEvent(t *testing.T) { dTags = append(dTags, tags) } - svc.HandleMultiPayKeysendEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.HandleMultiPayKeysendEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, 2, len(responses)) for i := 0; i < len(responses); i++ { @@ -599,7 +390,7 @@ func TestHandleMultiPayKeysendEvent(t *testing.T) { requestEvent.NostrId = reqEvent.ID responses = []*nip47.Response{} dTags = []nostr.Tags{} - svc.HandleMultiPayKeysendEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.HandleMultiPayKeysendEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, 2, len(responses)) for i := 0; i < len(responses); i++ { @@ -625,7 +416,7 @@ func TestHandleMultiPayKeysendEvent(t *testing.T) { requestEvent.NostrId = reqEvent.ID responses = []*nip47.Response{} dTags = []nostr.Tags{} - svc.HandleMultiPayKeysendEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.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()) @@ -635,10 +426,8 @@ func TestHandleMultiPayKeysendEvent(t *testing.T) { func TestHandleGetBalanceEvent(t *testing.T) { ctx := context.TODO() - defer os.Remove(testDB) - mockLn, err := NewMockLn() - assert.NoError(t, err) - svc, err := createTestService(mockLn) + defer removeTestService() + svc, err := createTestService() assert.NoError(t, err) app, ss, err := createApp(svc) assert.NoError(t, err) @@ -670,7 +459,7 @@ func TestHandleGetBalanceEvent(t *testing.T) { err = svc.db.Create(&requestEvent).Error assert.NoError(t, err) - svc.HandleGetBalanceEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.HandleGetBalanceEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, responses[0].Error.Code, nip47.ERROR_RESTRICTED) @@ -688,7 +477,7 @@ func TestHandleGetBalanceEvent(t *testing.T) { reqEvent.ID = "test_get_balance_with_permission" requestEvent.NostrId = reqEvent.ID responses = []*nip47.Response{} - svc.HandleGetBalanceEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.HandleGetBalanceEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, responses[0].Result.(*nip47.BalanceResponse).Balance, int64(21000)) @@ -708,7 +497,7 @@ func TestHandleGetBalanceEvent(t *testing.T) { reqEvent.ID = "test_get_balance_with_budget" responses = []*nip47.Response{} - svc.HandleGetBalanceEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.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) @@ -717,10 +506,8 @@ func TestHandleGetBalanceEvent(t *testing.T) { func TestHandlePayInvoiceEvent(t *testing.T) { ctx := context.TODO() - defer os.Remove(testDB) - mockLn, err := NewMockLn() - assert.NoError(t, err) - svc, err := createTestService(mockLn) + defer removeTestService() + svc, err := createTestService() assert.NoError(t, err) app, ss, err := createApp(svc) assert.NoError(t, err) @@ -749,7 +536,7 @@ func TestHandlePayInvoiceEvent(t *testing.T) { responses = append(responses, response) } - svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, nip47.ERROR_RESTRICTED, responses[0].Error.Code) @@ -771,7 +558,7 @@ func TestHandlePayInvoiceEvent(t *testing.T) { reqEvent.ID = "pay_invoice_with_permission" requestEvent.NostrId = reqEvent.ID responses = []*nip47.Response{} - svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, responses[0].Result.(nip47.PayResponse).Preimage, "123preimage") @@ -786,7 +573,7 @@ func TestHandlePayInvoiceEvent(t *testing.T) { reqEvent.ID = "pay_invoice_with_malformed_invoice" requestEvent.NostrId = reqEvent.ID responses = []*nip47.Response{} - svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, nip47.ERROR_INTERNAL, responses[0].Error.Code) @@ -801,7 +588,7 @@ func TestHandlePayInvoiceEvent(t *testing.T) { reqEvent.ID = "pay_invoice_with_wrong_request_method" requestEvent.NostrId = reqEvent.ID responses = []*nip47.Response{} - svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, nip47.ERROR_RESTRICTED, responses[0].Error.Code) @@ -820,7 +607,7 @@ func TestHandlePayInvoiceEvent(t *testing.T) { reqEvent.ID = "pay_invoice_with_budget_overflow" requestEvent.NostrId = reqEvent.ID responses = []*nip47.Response{} - svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, nip47.ERROR_QUOTA_EXCEEDED, responses[0].Error.Code) @@ -832,7 +619,7 @@ func TestHandlePayInvoiceEvent(t *testing.T) { reqEvent.ID = "pay_invoice_with_budget_expiry" requestEvent.NostrId = reqEvent.ID responses = []*nip47.Response{} - svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, nip47.ERROR_EXPIRED, responses[0].Error.Code) @@ -843,17 +630,15 @@ func TestHandlePayInvoiceEvent(t *testing.T) { reqEvent.ID = "pay_invoice_after_change" requestEvent.NostrId = reqEvent.ID responses = []*nip47.Response{} - svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.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) + defer removeTestService() + svc, err := createTestService() assert.NoError(t, err) app, ss, err := createApp(svc) assert.NoError(t, err) @@ -882,7 +667,7 @@ func TestHandlePayKeysendEvent(t *testing.T) { responses = append(responses, response) } - svc.HandlePayKeysendEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.HandlePayKeysendEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, nip47.ERROR_RESTRICTED, responses[0].Error.Code) @@ -907,7 +692,7 @@ func TestHandlePayKeysendEvent(t *testing.T) { reqEvent.ID = "pay_keysend_with_permission" requestEvent.NostrId = reqEvent.ID responses = []*nip47.Response{} - svc.HandlePayKeysendEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.HandlePayKeysendEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, responses[0].Result.(nip47.PayResponse).Preimage, "12345preimage") @@ -926,17 +711,15 @@ func TestHandlePayKeysendEvent(t *testing.T) { reqEvent.ID = "pay_keysend_with_budget_overflow" requestEvent.NostrId = reqEvent.ID responses = []*nip47.Response{} - svc.HandlePayKeysendEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.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) + defer removeTestService() + svc, err := createTestService() assert.NoError(t, err) app, ss, err := createApp(svc) assert.NoError(t, err) @@ -965,7 +748,7 @@ func TestHandleLookupInvoiceEvent(t *testing.T) { responses = append(responses, response) } - svc.HandleLookupInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.HandleLookupInvoiceEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, nip47.ERROR_RESTRICTED, responses[0].Error.Code) @@ -983,7 +766,7 @@ func TestHandleLookupInvoiceEvent(t *testing.T) { reqEvent.ID = "test_lookup_invoice_with_permission" requestEvent.NostrId = reqEvent.ID responses = []*nip47.Response{} - svc.HandleLookupInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.HandleLookupInvoiceEvent(ctx, request, requestEvent, app, publishResponse) transaction := responses[0].Result.(*nip47.LookupInvoiceResponse) assert.Equal(t, mockTransaction.Type, transaction.Type) @@ -999,10 +782,8 @@ func TestHandleLookupInvoiceEvent(t *testing.T) { func TestHandleMakeInvoiceEvent(t *testing.T) { ctx := context.TODO() - defer os.Remove(testDB) - mockLn, err := NewMockLn() - assert.NoError(t, err) - svc, err := createTestService(mockLn) + defer removeTestService() + svc, err := createTestService() assert.NoError(t, err) app, ss, err := createApp(svc) assert.NoError(t, err) @@ -1031,7 +812,7 @@ func TestHandleMakeInvoiceEvent(t *testing.T) { responses = append(responses, response) } - svc.HandleMakeInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.HandleMakeInvoiceEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, nip47.ERROR_RESTRICTED, responses[0].Error.Code) @@ -1049,17 +830,15 @@ func TestHandleMakeInvoiceEvent(t *testing.T) { reqEvent.ID = "test_make_invoice_with_permission" requestEvent.NostrId = reqEvent.ID responses = []*nip47.Response{} - svc.HandleMakeInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.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) + defer removeTestService() + svc, err := createTestService() assert.NoError(t, err) app, ss, err := createApp(svc) assert.NoError(t, err) @@ -1088,7 +867,7 @@ func TestHandleListTransactionsEvent(t *testing.T) { responses = append(responses, response) } - svc.HandleListTransactionsEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.HandleListTransactionsEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, nip47.ERROR_RESTRICTED, responses[0].Error.Code) @@ -1106,7 +885,7 @@ func TestHandleListTransactionsEvent(t *testing.T) { reqEvent.ID = "test_list_transactions_with_permission" requestEvent.NostrId = reqEvent.ID responses = []*nip47.Response{} - svc.HandleListTransactionsEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.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] @@ -1123,10 +902,8 @@ func TestHandleListTransactionsEvent(t *testing.T) { func TestHandleGetInfoEvent(t *testing.T) { ctx := context.TODO() - defer os.Remove(testDB) - mockLn, err := NewMockLn() - assert.NoError(t, err) - svc, err := createTestService(mockLn) + defer removeTestService() + svc, err := createTestService() assert.NoError(t, err) app, ss, err := createApp(svc) assert.NoError(t, err) @@ -1155,7 +932,7 @@ func TestHandleGetInfoEvent(t *testing.T) { responses = append(responses, response) } - svc.HandleGetInfoEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.HandleGetInfoEvent(ctx, request, requestEvent, app, publishResponse) assert.Equal(t, nip47.ERROR_RESTRICTED, responses[0].Error.Code) @@ -1172,7 +949,7 @@ func TestHandleGetInfoEvent(t *testing.T) { reqEvent.ID = "test_get_info_with_permission" requestEvent.NostrId = reqEvent.ID responses = []*nip47.Response{} - svc.HandleGetInfoEvent(ctx, request, requestEvent, app, publishResponse) + svc.nip47Svc.HandleGetInfoEvent(ctx, request, requestEvent, app, publishResponse) nodeInfo := responses[0].Result.(*nip47.GetInfoResponse) assert.Equal(t, mockNodeInfo.Alias, nodeInfo.Alias) @@ -1183,160 +960,3 @@ func TestHandleGetInfoEvent(t *testing.T) { 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/nip47_notifier_test.go b/tests/nip47_notifier_test.go similarity index 74% rename from nip47_notifier_test.go rename to tests/nip47_notifier_test.go index ce156952b..8c6b3772c 100644 --- a/nip47_notifier_test.go +++ b/tests/nip47_notifier_test.go @@ -1,10 +1,9 @@ -package main +package tests import ( "context" "encoding/json" "log" - "os" "testing" "github.com/getAlby/nostr-wallet-connect/db" @@ -17,11 +16,15 @@ import ( func TestSendNotification(t *testing.T) { ctx := context.TODO() - defer os.Remove(testDB) - mockLn, err := NewMockLn() + defer removeTestService() + svc, err := createTestService() assert.NoError(t, err) - svc, err := createTestService(mockLn) + + /*mockLn, err := NewMockLn() assert.NoError(t, err) + svc, err := createTestService(mockLn) + assert.NoError(t, err)*/ + app, ss, err := createApp(svc) assert.NoError(t, err) @@ -33,8 +36,8 @@ func TestSendNotification(t *testing.T) { err = svc.db.Create(appPermission).Error assert.NoError(t, err) - svc.nip47NotificationQueue = nip47.NewNip47NotificationQueue(svc.logger) - svc.eventPublisher.RegisterSubscriber(svc.nip47NotificationQueue) + nip47NotificationQueue := nip47.NewNip47NotificationQueue(svc.logger) + svc.eventPublisher.RegisterSubscriber(nip47NotificationQueue) testEvent := &events.Event{ Event: "nwc_payment_received", @@ -47,13 +50,15 @@ func TestSendNotification(t *testing.T) { svc.eventPublisher.Publish(testEvent) - receivedEvent := <-svc.nip47NotificationQueue.Channel() + receivedEvent := <-nip47NotificationQueue.Channel() assert.Equal(t, testEvent, receivedEvent) relay := NewMockRelay() - n := NewNip47Notifier(svc, relay) - n.ConsumeEvent(ctx, receivedEvent) + nip47Svc := nip47.NewNip47Service(svc.db, svc.logger, svc.eventPublisher, svc.cfg, svc.lnClient) + + notifier := nip47.NewNip47Notifier(nip47Svc, relay, svc.logger, svc.cfg, svc.db, svc.lnClient) + notifier.ConsumeEvent(ctx, receivedEvent) assert.NotNil(t, relay.publishedEvent) assert.NotEmpty(t, relay.publishedEvent.Content) @@ -82,16 +87,14 @@ func TestSendNotification(t *testing.T) { func TestSendNotificationNoPermission(t *testing.T) { ctx := context.TODO() - defer os.Remove(testDB) - mockLn, err := NewMockLn() - assert.NoError(t, err) - svc, err := createTestService(mockLn) + defer removeTestService() + svc, err := createTestService() assert.NoError(t, err) _, _, err = createApp(svc) assert.NoError(t, err) - svc.nip47NotificationQueue = nip47.NewNip47NotificationQueue(svc.logger) - svc.eventPublisher.RegisterSubscriber(svc.nip47NotificationQueue) + nip47NotificationQueue := nip47.NewNip47NotificationQueue(svc.logger) + svc.eventPublisher.RegisterSubscriber(nip47NotificationQueue) testEvent := &events.Event{ Event: "nwc_payment_received", @@ -104,13 +107,15 @@ func TestSendNotificationNoPermission(t *testing.T) { svc.eventPublisher.Publish(testEvent) - receivedEvent := <-svc.nip47NotificationQueue.Channel() + receivedEvent := <-nip47NotificationQueue.Channel() assert.Equal(t, testEvent, receivedEvent) relay := NewMockRelay() - n := NewNip47Notifier(svc, relay) - n.ConsumeEvent(ctx, receivedEvent) + nip47Svc := nip47.NewNip47Service(svc.db, svc.logger, svc.eventPublisher, svc.cfg, svc.lnClient) + + notifier := nip47.NewNip47Notifier(nip47Svc, relay, svc.logger, svc.cfg, svc.db, svc.lnClient) + notifier.ConsumeEvent(ctx, receivedEvent) assert.Nil(t, relay.publishedEvent) } diff --git a/tests/nip47_permissions_test.go b/tests/nip47_permissions_test.go new file mode 100644 index 000000000..43532445f --- /dev/null +++ b/tests/nip47_permissions_test.go @@ -0,0 +1,105 @@ +package tests + +import ( + "testing" + "time" + + "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/nip47" + "github.com/stretchr/testify/assert" +) + +func TestHasPermission_NoPermission(t *testing.T) { + defer removeTestService() + svc, err := createTestService() + assert.NoError(t, err) + + app, _, err := createApp(svc) + assert.NoError(t, err) + + result, code, message := svc.nip47Svc.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 removeTestService() + svc, err := createTestService() + 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.nip47Svc.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 removeTestService() + svc, err := createTestService() + 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.nip47Svc.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 removeTestService() + svc, err := createTestService() + 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.nip47Svc.HasPermission(app, nip47.PAY_INVOICE_METHOD, 10*1000) + assert.True(t, result) + assert.Empty(t, code) + assert.Empty(t, message) +} 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_app.go b/wails/wails_app.go similarity index 86% rename from wails_app.go rename to wails/wails_app.go index 5ffe3ca08..b90c0a945 100644 --- a/wails_app.go +++ b/wails/wails_app.go @@ -1,4 +1,4 @@ -package main +package wails import ( "context" @@ -6,6 +6,7 @@ import ( "log" "github.com/getAlby/nostr-wallet-connect/api" + "github.com/getAlby/nostr-wallet-connect/service" "github.com/sirupsen/logrus" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" @@ -14,22 +15,16 @@ import ( "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 + svc service.Service api api.API } -func NewApp(svc *Service) *WailsApp { +func NewApp(svc service.Service) *WailsApp { return &WailsApp{ svc: svc, - api: api.NewAPI(svc, svc.logger, svc.db), + api: api.NewAPI(svc, svc.GetLogger(), svc.GetDB(), svc.GetConfig()), } } @@ -39,8 +34,8 @@ func (app *WailsApp) startup(ctx context.Context) { app.ctx = ctx } -func LaunchWailsApp(app *WailsApp) { - logger := NewWailsLogger(app.svc.logger) +func LaunchWailsApp(app *WailsApp, assets embed.FS, appIcon []byte) { + logger := NewWailsLogger(app.svc.GetLogger()) err := wails.Run(&options.App{ Title: "AlbyHub", 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..62b3d04d2 100644 --- a/wails_handlers.go +++ b/wails/wails_handlers.go @@ -1,4 +1,4 @@ -package main +package wails import ( "encoding/json" @@ -12,7 +12,6 @@ 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/wailsapp/wails/v2/pkg/runtime" ) @@ -36,9 +35,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{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -59,7 +58,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string pubkey := appMatch[1] userApp := db.App{} - findResult := app.svc.db.Where("nostr_pubkey = ?", pubkey).First(&userApp) + findResult := app.svc.GetDB().Where("nostr_pubkey = ?", pubkey).First(&userApp) if findResult.RowsAffected == 0 { return WailsRequestRouterResponse{Body: nil, Error: "App does not exist"} @@ -73,7 +72,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string updateAppRequest := &api.UpdateAppRequest{} err := json.Unmarshal([]byte(body), updateAppRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -168,9 +167,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{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -179,9 +178,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{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -195,14 +194,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{ + app.svc.GetLogger().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 +218,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string createAppRequest := &api.CreateAppRequest{} err := json.Unmarshal([]byte(body), createAppRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -236,7 +235,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string resetRouterRequest := &api.ResetRouterRequest{} err := json.Unmarshal([]byte(body), resetRouterRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -270,7 +269,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string openChannelRequest := &api.OpenChannelRequest{} err := json.Unmarshal([]byte(body), openChannelRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -317,7 +316,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string redeemOnchainFundsRequest := &api.RedeemOnchainFundsRequest{} err := json.Unmarshal([]byte(body), redeemOnchainFundsRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -340,13 +339,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 +358,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string connectPeerRequest := &api.ConnectPeerRequest{} err := json.Unmarshal([]byte(body), connectPeerRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -393,7 +392,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 +406,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string backupReminderRequest := &api.BackupReminderRequest{} err := json.Unmarshal([]byte(body), backupReminderRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -417,7 +416,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string err = app.api.SetNextBackupReminder(backupReminderRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -429,7 +428,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string changeUnlockPasswordRequest := &api.ChangeUnlockPasswordRequest{} err := json.Unmarshal([]byte(body), changeUnlockPasswordRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -439,7 +438,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string err = app.api.ChangeUnlockPassword(changeUnlockPasswordRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -451,7 +450,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string startRequest := &api.StartRequest{} err := json.Unmarshal([]byte(body), startRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -460,7 +459,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string } err = app.api.Start(startRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -474,7 +473,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string setupRequest := &api.SetupRequest{} err := json.Unmarshal([]byte(body), setupRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -483,7 +482,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string } err = app.api.Setup(ctx, setupRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -495,7 +494,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string sendPaymentProbesRequest := &api.SendPaymentProbesRequest{} err := json.Unmarshal([]byte(body), sendPaymentProbesRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -504,7 +503,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string } sendPaymentProbesResponse, err := app.api.SendPaymentProbes(ctx, sendPaymentProbesRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -516,7 +515,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string sendSpontaneousPaymentProbesRequest := &api.SendSpontaneousPaymentProbesRequest{} err := json.Unmarshal([]byte(body), sendSpontaneousPaymentProbesRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -525,7 +524,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string } sendSpontaneousPaymentProbesResponse, err := app.api.SendSpontaneousPaymentProbes(ctx, sendSpontaneousPaymentProbesRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -537,7 +536,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string backupRequest := &api.BasicBackupRequest{} err := json.Unmarshal([]byte(body), backupRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -550,7 +549,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string DefaultFilename: "nwc.bkp", }) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -560,7 +559,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string backupFile, err := os.Create(saveFilePath) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -570,10 +569,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{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -585,7 +584,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string restoreRequest := &api.BasicRestoreWailsRequest{} err := json.Unmarshal([]byte(body), restoreRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -598,7 +597,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string DefaultFilename: "nwc.bkp", }) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -608,7 +607,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string backupFile, err := os.Open(backupFilePath) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -618,9 +617,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{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -638,7 +637,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string getLogOutputRequest := &api.GetLogOutputRequest{} err := json.Unmarshal([]byte(body), getLogOutputRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -647,7 +646,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string } logOutputResponse, err := app.api.GetLogOutput(ctx, logType, getLogOutputRequest) if err != nil { - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -657,7 +656,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string return WailsRequestRouterResponse{Body: logOutputResponse, Error: ""} } - app.svc.logger.WithFields(logrus.Fields{ + app.svc.GetLogger().WithFields(logrus.Fields{ "route": route, "method": method, }).Error("Unhandled route") From f299adcf6367d364e72b8b34c4beb99a2f4d4e64 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Fri, 14 Jun 2024 10:37:34 +0700 Subject: [PATCH 02/16] fix: include errors in payment failed events --- nip47/handle_multi_pay_invoice_request.go | 2 +- nip47/handle_multi_pay_keysend_request.go | 2 +- nip47/handle_pay_keysend_request.go | 2 +- nip47/handle_payment_request.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nip47/handle_multi_pay_invoice_request.go b/nip47/handle_multi_pay_invoice_request.go index fa09658ec..3f22d28a3 100644 --- a/nip47/handle_multi_pay_invoice_request.go +++ b/nip47/handle_multi_pay_invoice_request.go @@ -95,7 +95,7 @@ func (svc *nip47Service) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Re svc.eventPublisher.Publish(&events.Event{ Event: "nwc_payment_failed", Properties: map[string]interface{}{ - // "error": fmt.Sprintf("%v", err), + "error": err.Error(), "multi": true, "invoice": bolt11, "amount": paymentRequest.MSatoshi / 1000, diff --git a/nip47/handle_multi_pay_keysend_request.go b/nip47/handle_multi_pay_keysend_request.go index 2dac7e30f..79d54c40e 100644 --- a/nip47/handle_multi_pay_keysend_request.go +++ b/nip47/handle_multi_pay_keysend_request.go @@ -67,7 +67,7 @@ func (svc *nip47Service) HandleMultiPayKeysendEvent(ctx context.Context, nip47Re svc.eventPublisher.Publish(&events.Event{ Event: "nwc_payment_failed", Properties: map[string]interface{}{ - // "error": fmt.Sprintf("%v", err), + "error": err.Error(), "keysend": true, "multi": true, "amount": keysendInfo.Amount / 1000, diff --git a/nip47/handle_pay_keysend_request.go b/nip47/handle_pay_keysend_request.go index 52d7e3f0c..b48504d7c 100644 --- a/nip47/handle_pay_keysend_request.go +++ b/nip47/handle_pay_keysend_request.go @@ -53,7 +53,7 @@ func (svc *nip47Service) HandlePayKeysendEvent(ctx context.Context, nip47Request svc.eventPublisher.Publish(&events.Event{ Event: "nwc_payment_failed", Properties: map[string]interface{}{ - // "error": fmt.Sprintf("%v", err), + "error": err.Error(), "keysend": true, "amount": payParams.Amount / 1000, }, diff --git a/nip47/handle_payment_request.go b/nip47/handle_payment_request.go index 71f381cf0..b228d12e4 100644 --- a/nip47/handle_payment_request.go +++ b/nip47/handle_payment_request.go @@ -77,7 +77,7 @@ func (svc *nip47Service) HandlePayInvoiceEvent(ctx context.Context, nip47Request svc.eventPublisher.Publish(&events.Event{ Event: "nwc_payment_failed", Properties: map[string]interface{}{ - // "error": fmt.Sprintf("%v", err), + "error": err.Error(), "invoice": bolt11, "amount": paymentRequest.MSatoshi / 1000, }, From 33ba59d75c373fcf3bfc89350ec3ec589ff337b7 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Sat, 15 Jun 2024 13:54:39 +0700 Subject: [PATCH 03/16] chore: use global logger --- alby/alby_http_service.go | 16 +- alby/alby_oauth_service.go | 119 ++++++------ api/api.go | 59 +++--- api/backup.go | 27 +-- api/esplora.go | 9 +- api/lsp.go | 99 +++++----- config/config.go | 30 ++- db/db_service.go | 14 +- events/events.go | 19 +- http/http_service.go | 18 +- lnclient/breez/breez.go | 34 ++-- lnclient/breez/breez_stub.go | 4 +- lnclient/cashu/cashu.go | 31 ++- lnclient/greenlight/greenlight.go | 65 ++++--- lnclient/ldk/ldk.go | 177 +++++++++--------- lnclient/ldk/ldk_event_broadcaster.go | 18 +- lnclient/lnd/lnd.go | 38 ++-- lnclient/phoenixd/phoenixd.go | 12 +- logger/logger.go | 52 +++++ main_http.go | 13 +- main_wails.go | 7 +- .../202403171120_delete_ldk_payments.go | 7 +- migrations/migrate.go | 5 +- nip47/event_handler.go | 55 +++--- nip47/handle_balance_request.go | 5 +- nip47/handle_info_request.go | 5 +- nip47/handle_list_transactions_request.go | 5 +- nip47/handle_lookup_invoice_request.go | 7 +- nip47/handle_make_invoice_request.go | 5 +- nip47/handle_multi_pay_invoice_request.go | 9 +- nip47/handle_multi_pay_keysend_request.go | 7 +- nip47/handle_pay_keysend_request.go | 5 +- nip47/handle_payment_request.go | 7 +- nip47/handle_sign_message_request.go | 5 +- nip47/nip47_notification_queue.go | 8 +- nip47/nip47_notifier.go | 23 ++- nip47/nip47_service.go | 9 +- nip47/permissions.go | 5 +- service/models.go | 3 - service/service.go | 116 ++++-------- service/start.go | 37 ++-- tests/create_test_service.go | 18 +- tests/nip47_notifier_test.go | 12 +- wails/wails_app.go | 13 +- wails/wails_handlers.go | 66 +++---- 45 files changed, 651 insertions(+), 647 deletions(-) create mode 100644 logger/logger.go 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..00c24ba70 100644 --- a/alby/alby_oauth_service.go +++ b/alby/alby_oauth_service.go @@ -18,6 +18,7 @@ import ( "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/logger" "github.com/getAlby/nostr-wallet-connect/nip47" ) @@ -25,7 +26,6 @@ type albyOAuthService struct { appConfig *config.AppConfig config config.Config oauthConf *oauth2.Config - logger *logrus.Logger dbSvc db.DBService } @@ -36,7 +36,7 @@ const ( userIdentifierKey = "AlbyUserIdentifier" ) -func NewAlbyOAuthService(logger *logrus.Logger, config config.Config, appConfig *config.AppConfig, dbSvc db.DBService) *albyOAuthService { +func NewAlbyOAuthService(config config.Config, appConfig *config.AppConfig, dbSvc db.DBService) *albyOAuthService { conf := &oauth2.Config{ ClientID: appConfig.AlbyClientId, ClientSecret: appConfig.AlbyClientSecret, @@ -58,7 +58,6 @@ func NewAlbyOAuthService(logger *logrus.Logger, config config.Config, appConfig appConfig: appConfig, oauthConf: conf, config: config, - logger: logger, dbSvc: dbSvc, } return albyOAuthSvc @@ -67,14 +66,14 @@ 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, "", "") return err @@ -82,7 +81,7 @@ func (svc *albyOAuthService) CallbackHandler(ctx context.Context, code string) e 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 } @@ -101,7 +100,7 @@ func (svc *albyOAuthService) CallbackHandler(ctx context.Context, code string) e func (svc *albyOAuthService) GetUserIdentifier() (string, error) { userIdentifier, err := svc.config.Get(userIdentifierKey, "") if err != nil { - svc.logger.WithError(err).Error("Failed to fetch user identifier from user configs") + logger.Logger.WithError(err).Error("Failed to fetch user identifier from user configs") return "", err } return userIdentifier, nil @@ -110,7 +109,7 @@ 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 } @@ -164,13 +163,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,7 +180,7 @@ 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 } @@ -189,7 +188,7 @@ func (svc *albyOAuthService) GetMe(ctx context.Context) (*AlbyMe, error) { req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/users", svc.appConfig.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 +196,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,7 +215,7 @@ 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 } @@ -224,7 +223,7 @@ func (svc *albyOAuthService) GetBalance(ctx context.Context) (*AlbyBalance, erro req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/lndhub/balance", svc.appConfig.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 +231,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 +265,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) 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 +280,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 +302,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 +319,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, @@ -333,7 +332,7 @@ 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") + logger.Logger.Fatalf("No ALBY_OAUTH_CLIENT_ID or ALBY_OAUTH_CLIENT_SECRET set") } return svc.oauthConf.AuthCodeURL("unused") } @@ -341,7 +340,7 @@ 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 } @@ -355,17 +354,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 } @@ -375,13 +374,13 @@ 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") + 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 +388,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 +399,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 +411,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 +422,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 +432,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) 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 +447,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") @@ -532,7 +531,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) @@ -549,13 +548,13 @@ func (svc *albyOAuthService) createAlbyAccountNWCNode(ctx context.Context) (stri 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) 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 +563,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 +584,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 +598,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) 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 +614,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,7 +634,7 @@ 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 } @@ -643,7 +642,7 @@ func (svc *albyOAuthService) GetChannelPeerSuggestions(ctx context.Context) ([]C req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/channel_suggestions", svc.appConfig.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 +650,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 e0eaa4a8e..6c54b4f6c 100644 --- a/api/api.go +++ b/api/api.go @@ -18,26 +18,25 @@ import ( "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/logger" "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/getAlby/nostr-wallet-connect/service" "github.com/getAlby/nostr-wallet-connect/utils" ) type api struct { - logger *logrus.Logger - db *gorm.DB - dbSvc db.DBService - cfg config.Config - svc service.Service + db *gorm.DB + dbSvc db.DBService + cfg config.Config + svc service.Service } -func NewAPI(svc service.Service, logger *logrus.Logger, gormDb *gorm.DB, config config.Config) *api { +func NewAPI(svc service.Service, gormDb *gorm.DB, config config.Config) *api { return &api{ - logger: logger, - db: gormDb, - dbSvc: db.NewDBService(gormDb, logger), - cfg: config, - svc: svc, + db: gormDb, + dbSvc: db.NewDBService(gormDb), + cfg: config, + svc: svc, } } @@ -295,7 +294,7 @@ func (api *api) ChangeUnlockPassword(changeUnlockPasswordRequest *ChangeUnlockPa 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 } @@ -305,7 +304,7 @@ 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") } @@ -315,7 +314,7 @@ func (api *api) Stop() error { // 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 @@ -360,7 +359,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) @@ -370,7 +369,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, @@ -403,7 +402,7 @@ func (api *api) GetUnusedOnchainAddress(ctx context.Context) (string, error) { 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 } @@ -411,13 +410,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 } @@ -429,7 +428,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 @@ -483,7 +482,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 @@ -491,7 +490,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 @@ -501,7 +500,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") @@ -510,7 +509,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)) @@ -529,7 +528,7 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) { info.OAuthRedirect = !api.cfg.GetEnv().IsDefaultClientId() albyUserIdentifier, err := api.svc.GetAlbyOAuthSvc().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 @@ -537,7 +536,7 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) { 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 } @@ -568,11 +567,11 @@ 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") } @@ -676,7 +675,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 { @@ -695,7 +694,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()) diff --git a/api/backup.go b/api/backup.go index 611373808..65e81feb1 100644 --- a/api/backup.go +++ b/api/backup.go @@ -17,6 +17,7 @@ import ( "crypto/rand" "crypto/sha256" + "github.com/getAlby/nostr-wallet-connect/logger" "golang.org/x/crypto/pbkdf2" ) @@ -41,12 +42,12 @@ func (bs *api) CreateBackup(unlockPassword string, w io.Writer) error { 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") 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. @@ -57,7 +58,7 @@ func (bs *api) CreateBackup(unlockPassword string, w io.Writer) error { // to be used after its data is exported. err = bs.svc.StopDb() 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) } @@ -68,7 +69,7 @@ func (bs *api) 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 { @@ -105,25 +106,25 @@ func (bs *api) CreateBackup(unlockPassword string, w io.Writer) error { // Locate the main database file. dbFilePath := bs.svc.GetConfig().GetEnv().DatabaseUri // Add the database file to the archive. - bs.logger.WithField("nwc.db", dbFilePath).Info("adding nwc db to zip") + 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) } } @@ -198,17 +199,17 @@ func (bs *api) 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 210163d5d..8dd0e31a4 100644 --- a/api/esplora.go +++ b/api/esplora.go @@ -8,6 +8,7 @@ import ( "net/http" "time" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/sirupsen/logrus" ) @@ -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/api/lsp.go b/api/lsp.go index 6e43228f9..a1dd411d4 100644 --- a/api/lsp.go +++ b/api/lsp.go @@ -14,6 +14,7 @@ import ( "time" "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/getAlby/nostr-wallet-connect/lsp" "github.com/sirupsen/logrus" ) @@ -55,7 +56,7 @@ func (ls *api) NewInstantChannelInvoice(ctx context.Context, request *NewInstant 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 *lspConnectionInfo var err error @@ -72,21 +73,21 @@ func (ls *api) NewInstantChannelInvoice(ctx context.Context, request *NewInstant 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) 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{ Pubkey: lspInfo.Pubkey, @@ -95,7 +96,7 @@ func (ls *api) NewInstantChannelInvoice(ctx context.Context, request *NewInstant }) 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 } @@ -114,7 +115,7 @@ func (ls *api) NewInstantChannelInvoice(ctx context.Context, request *NewInstant 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 } @@ -123,7 +124,7 @@ func (ls *api) NewInstantChannelInvoice(ctx context.Context, request *NewInstant Fee: fee, } - ls.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "newChannelResponse": newChannelResponse, }).Info("New Channel response") @@ -142,7 +143,7 @@ func (ls *api) getLSPS1LSPInfo(url string) (*lspConnectionInfo, 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 @@ -150,7 +151,7 @@ func (ls *api) getLSPS1LSPInfo(url string) (*lspConnectionInfo, 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 @@ -160,7 +161,7 @@ func (ls *api) getLSPS1LSPInfo(url string) (*lspConnectionInfo, 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") @@ -168,7 +169,7 @@ func (ls *api) getLSPS1LSPInfo(url string) (*lspConnectionInfo, 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)) @@ -179,15 +180,15 @@ func (ls *api) getLSPS1LSPInfo(url string) (*lspConnectionInfo, 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 } @@ -214,7 +215,7 @@ func (ls *api) getFlowLSPInfo(url string) (*lspConnectionInfo, 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 @@ -222,7 +223,7 @@ func (ls *api) getFlowLSPInfo(url string) (*lspConnectionInfo, 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 @@ -232,7 +233,7 @@ func (ls *api) getFlowLSPInfo(url string) (*lspConnectionInfo, 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") @@ -240,7 +241,7 @@ func (ls *api) getFlowLSPInfo(url string) (*lspConnectionInfo, 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)) @@ -255,7 +256,7 @@ func (ls *api) getFlowLSPInfo(url string) (*lspConnectionInfo, 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") } @@ -267,7 +268,7 @@ func (ls *api) getFlowLSPInfo(url string) (*lspConnectionInfo, error) { } func (ls *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *lsp.LSP, amount uint64, pubkey string) (invoice string, fee uint64, err error) { - ls.logger.Infoln("Requesting fee information") + logger.Logger.Infoln("Requesting fee information") type FeeRequest struct { AmountMsat uint64 `json:"amount_msat"` @@ -294,7 +295,7 @@ func (ls *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *lsp 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 @@ -304,7 +305,7 @@ func (ls *api) requestFlow20WrappedInvoice(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 lsp fee") return "", 0, err @@ -314,14 +315,14 @@ func (ls *api) requestFlow20WrappedInvoice(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{ "body": string(body), "statusCode": res.StatusCode, }).Error("fee endpoint returned non-success code") @@ -330,18 +331,18 @@ func (ls *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *lsp 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) @@ -353,7 +354,7 @@ func (ls *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *lsp // see: https://docs.voltage.cloud/voltage-lsp#gqBqV makeInvoiceResponse, err := ls.svc.GetLNClient().MakeInvoice(ctx, int64(amount)*1000-int64(feeResponse.FeeAmountMsat), "", "", 60*60) if err != nil { - ls.logger.WithError(err).Error("Failed to request own invoice") + logger.Logger.WithError(err).Error("Failed to request own invoice") return "", 0, fmt.Errorf("failed to request own invoice %v", err) } @@ -365,7 +366,7 @@ func (ls *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *lsp Bolt11 string `json:"jit_bolt11"` } - ls.logger.Infoln("Proposing invoice") + logger.Logger.Infoln("Proposing invoice") var proposalResponse ProposalResponse { @@ -383,7 +384,7 @@ func (ls *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *lsp 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 @@ -393,7 +394,7 @@ func (ls *api) requestFlow20WrappedInvoice(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 lsp fee") return "", 0, err @@ -403,14 +404,14 @@ func (ls *api) requestFlow20WrappedInvoice(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{ "body": string(body), "statusCode": res.StatusCode, }).Error("proposal endpoint returned non-success code") @@ -419,14 +420,14 @@ func (ls *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *lsp 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") @@ -458,7 +459,7 @@ func (ls *api) requestPMLSPInvoice(selectedLsp *lsp.LSP, amount uint64, pubkey s 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 @@ -468,7 +469,7 @@ func (ls *api) requestPMLSPInvoice(selectedLsp *lsp.LSP, amount uint64, pubkey s 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 @@ -478,14 +479,14 @@ func (ls *api) requestPMLSPInvoice(selectedLsp *lsp.LSP, amount uint64, pubkey s 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") @@ -501,7 +502,7 @@ func (ls *api) requestPMLSPInvoice(selectedLsp *lsp.LSP, amount uint64, pubkey s 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)) @@ -532,7 +533,7 @@ func (ls *api) requestLSPS1Invoice(ctx context.Context, selectedLsp *lsp.LSP, am refundAddress, err := ls.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 } @@ -564,7 +565,7 @@ func (ls *api) requestLSPS1Invoice(ctx context.Context, selectedLsp *lsp.LSP, am 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 @@ -574,7 +575,7 @@ func (ls *api) requestLSPS1Invoice(ctx context.Context, selectedLsp *lsp.LSP, am 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 @@ -584,14 +585,14 @@ func (ls *api) requestLSPS1Invoice(ctx context.Context, selectedLsp *lsp.LSP, am 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, @@ -611,7 +612,7 @@ func (ls *api) requestLSPS1Invoice(ctx context.Context, selectedLsp *lsp.LSP, am 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)) @@ -620,7 +621,7 @@ func (ls *api) requestLSPS1Invoice(ctx context.Context, selectedLsp *lsp.LSP, am 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/config/config.go b/config/config.go index f5130417c..599f644e0 100644 --- a/config/config.go +++ b/config/config.go @@ -8,8 +8,8 @@ import ( "os" "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/nbd-wtf/go-nostr" - "github.com/sirupsen/logrus" "gorm.io/gorm" "gorm.io/gorm/clause" ) @@ -20,23 +20,21 @@ type config struct { NostrSecretKey string NostrPublicKey string db *gorm.DB - logger *logrus.Logger } const ( unlockPasswordCheck = "THIS STRING SHOULD MATCH IF PASSWORD IS CORRECT" ) -func NewConfig(db *gorm.DB, env *AppConfig, logger *logrus.Logger) *config { +func NewConfig(db *gorm.DB, env *AppConfig) *config { cfg := &config{} - cfg.init(db, env, logger) + cfg.init(db, env) return cfg } -func (cfg *config) init(db *gorm.DB, env *AppConfig, logger *logrus.Logger) { +func (cfg *config) init(db *gorm.DB, env *AppConfig) { cfg.db = db cfg.Env = env - cfg.logger = logger if cfg.Env.Relay != "" { cfg.SetUpdate("Relay", cfg.Env.Relay, "") @@ -52,7 +50,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, "") @@ -60,7 +58,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, "") @@ -148,7 +146,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) } } @@ -159,7 +157,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) } } @@ -175,12 +173,12 @@ func (cfg *config) ChangeUnlockPassword(currentUnlockPassword string, newUnlockP 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{ @@ -189,10 +187,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 @@ -200,7 +198,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 } @@ -226,7 +224,7 @@ func (cfg *config) Start(encryptionKey string) error { } nostrPublicKey, err := nostr.GetPublicKey(nostrSecretKey) if err != nil { - cfg.logger.WithError(err).Error("Error converting nostr privkey to pubkey") + logger.Logger.WithError(err).Error("Error converting nostr privkey to pubkey") return err } cfg.NostrSecretKey = nostrSecretKey diff --git a/db/db_service.go b/db/db_service.go index edf9bb8a5..074becf73 100644 --- a/db/db_service.go +++ b/db/db_service.go @@ -5,20 +5,18 @@ import ( "fmt" "time" + "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, } } @@ -33,7 +31,7 @@ func (svc *dbService) CreateApp(name string, pubkey string, maxAmount int, budge //validate public key decoded, err := hex.DecodeString(pairingPublicKey) if err != nil || len(decoded) != 32 { - svc.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) } } @@ -65,7 +63,7 @@ func (svc *dbService) CreateApp(name string, pubkey string, maxAmount int, budge }) if err != nil { - svc.logger.WithError(err).Error("Failed to save app") + logger.Logger.WithError(err).Error("Failed to save app") return nil, "", err } 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/http/http_service.go b/http/http_service.go index 907048798..3910ec7c6 100644 --- a/http/http_service.go +++ b/http/http_service.go @@ -12,13 +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/logger" "github.com/getAlby/nostr-wallet-connect/service" "github.com/getAlby/nostr-wallet-connect/api" @@ -30,7 +30,6 @@ type HttpService struct { albyHttpSvc *alby.AlbyHttpService cfg config.Config db *gorm.DB - logger *logrus.Logger eventPublisher events.EventPublisher } @@ -39,13 +38,12 @@ const ( sessionCookieAuthKey = "authenticated" ) -func NewHttpService(svc service.Service, logger *logrus.Logger, db *gorm.DB, eventPublisher events.EventPublisher) *HttpService { +func NewHttpService(svc service.Service, db *gorm.DB, eventPublisher events.EventPublisher) *HttpService { return &HttpService{ - api: api.NewAPI(svc, logger, db, svc.GetConfig()), - albyHttpSvc: alby.NewAlbyHttpService(svc.GetAlbyOAuthSvc(), logger, svc.GetConfig().GetEnv()), + api: api.NewAPI(svc, db, svc.GetConfig()), + albyHttpSvc: alby.NewAlbyHttpService(svc.GetAlbyOAuthSvc(), svc.GetConfig().GetEnv()), cfg: svc.GetConfig(), db: db, - logger: logger, eventPublisher: eventPublisher, } } @@ -257,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 } @@ -410,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()), }) @@ -637,7 +635,7 @@ func (httpSvc *HttpService) appsUpdateHandler(c echo.Context) error { err := httpSvc.api.UpdateApp(&app, &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), }) @@ -685,7 +683,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), }) diff --git a/lnclient/breez/breez.go b/lnclient/breez/breez.go index 04615a6db..c864f7c0e 100644 --- a/lnclient/breez/breez.go +++ b/lnclient/breez/breez.go @@ -17,21 +17,20 @@ import ( "github.com/sirupsen/logrus" "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/getAlby/nostr-wallet-connect/nip47" ) 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 +38,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 +58,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 +71,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 +92,6 @@ func NewBreezService(logger *logrus.Logger, mnemonic, apiKey, inviteCode, workDi return &BreezService{ listener: &listener, svc: svc, - logger: logger, }, nil } @@ -344,10 +340,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 +353,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 +370,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 +379,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..47236fa54 100644 --- a/lnclient/cashu/cashu.go +++ b/lnclient/cashu/cashu.go @@ -12,17 +12,17 @@ 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/logger" "github.com/getAlby/nostr-wallet-connect/nip47" 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 +38,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 +61,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 +72,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 } @@ -109,13 +108,13 @@ func (cs *CashuService) GetBalance(ctx context.Context) (balance int64, err erro func (cs *CashuService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *nip47.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 @@ -128,7 +127,7 @@ func (cs *CashuService) LookupInvoice(ctx context.Context, paymentHash string) ( 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") } @@ -256,7 +255,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{ @@ -278,7 +277,7 @@ func (cs *CashuService) GetBalances(ctx context.Context) (*lnclient.BalancesResp func (cs *CashuService) cashuInvoiceToTransaction(cashuInvoice *storage.Invoice) (*nip47.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 @@ -318,19 +317,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..60610b618 100644 --- a/lnclient/greenlight/greenlight.go +++ b/lnclient/greenlight/greenlight.go @@ -21,18 +21,18 @@ import ( "github.com/getAlby/nostr-wallet-connect/config" "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/getAlby/nostr-wallet-connect/nip47" ) 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 +52,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 +90,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 +106,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 +118,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) @@ -148,7 +147,7 @@ func (gs *GreenlightService) SendKeysend(ctx context.Context, amount int64, dest }) 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 +158,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 } @@ -184,13 +183,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 @@ -219,7 +218,7 @@ func (gs *GreenlightService) LookupInvoice(ctx context.Context, paymentHash stri }) if err != nil { - gs.logger.Errorf("ListInvoices failed: %v", err) + logger.Logger.Errorf("ListInvoices failed: %v", err) return nil, err } @@ -235,7 +234,7 @@ 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 } @@ -246,7 +245,7 @@ func (gs *GreenlightService) ListTransactions(ctx context.Context, from, until, 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 } @@ -274,7 +273,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 +342,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 +360,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 +397,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 +420,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 +437,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 +451,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 +462,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 +474,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 +506,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,7 +536,7 @@ 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 } @@ -553,7 +552,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 @@ -605,14 +604,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..e54f69d1a 100644 --- a/lnclient/ldk/ldk.go +++ b/lnclient/ldk/ldk.go @@ -25,6 +25,7 @@ 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 +40,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 +101,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 +116,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 +131,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 +175,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 +188,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 +213,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 +227,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 +243,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 +275,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 +300,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 +323,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 +353,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 +380,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 +406,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 +419,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 +468,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 +482,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") @@ -510,7 +509,7 @@ func (ls *LDKService) SendKeysend(ctx context.Context, amount int64, destination paymentHash, err := ls.node.SpontaneousPayment().Send(uint64(amount), destination, customTlvs) if err != nil { - ls.logger.WithError(err).Error("Keysend failed") + logger.Logger.WithError(err).Error("Keysend failed") return "", err } @@ -523,23 +522,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 +571,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 +585,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") @@ -650,14 +649,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) @@ -686,14 +685,14 @@ func (ls *LDKService) LookupInvoice(ctx context.Context, paymentHash string) (tr 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 } @@ -711,7 +710,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 } @@ -747,7 +746,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 +771,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 +810,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 +824,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 +849,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 +870,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 +891,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 +906,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 +914,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 +927,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,7 +942,7 @@ 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 } @@ -951,7 +950,7 @@ func (ls *LDKService) SignMessage(ctx context.Context, message string) (string, } func (ls *LDKService) ldkPaymentToTransaction(payment *ldk_node.PaymentDetails) (*nip47.Transaction, error) { - // gs.logger.WithField("payment", payment).Debug("Mapping LDK payment to transaction") + // logger.Logger.WithField("payment", payment).Debug("Mapping LDK payment to transaction") transactionType := "incoming" if payment.Direction == ldk_node.PaymentDirectionOutbound { @@ -974,7 +973,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) @@ -1047,7 +1046,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 +1056,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 +1122,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 +1136,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 +1144,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 +1161,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 +1218,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 +1266,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 +1280,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..f7d0efbb7 100644 --- a/lnclient/lnd/lnd.go +++ b/lnclient/lnd/lnd.go @@ -17,6 +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/logger" "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/sirupsen/logrus" @@ -30,7 +31,6 @@ import ( type LNDService struct { client *wrapper.LNDWrapper // db *gorm.DB - Logger *logrus.Logger } func (svc *LNDService) GetBalance(ctx context.Context) (balance int64, err error) { @@ -83,7 +83,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) @@ -252,7 +252,7 @@ func (svc *LNDService) MakeInvoice(ctx context.Context, amount int64, descriptio 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, @@ -280,7 +280,7 @@ func (svc *LNDService) LookupInvoice(ctx context.Context, paymentHash string) (t 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") @@ -334,7 +334,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, @@ -365,7 +365,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 +376,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 +388,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 +398,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 +419,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 +430,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 +438,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 +487,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 +516,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 +564,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 +578,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 +589,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 +641,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 } diff --git a/lnclient/phoenixd/phoenixd.go b/lnclient/phoenixd/phoenixd.go index 49ac69dec..992c2bf5d 100644 --- a/lnclient/phoenixd/phoenixd.go +++ b/lnclient/phoenixd/phoenixd.go @@ -13,6 +13,7 @@ import ( "time" "github.com/getAlby/nostr-wallet-connect/lnclient" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/sirupsen/logrus" @@ -67,12 +68,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 } @@ -138,7 +138,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) @@ -197,7 +197,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) @@ -288,7 +288,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") 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/main_http.go b/main_http.go index efa42d246..bac94e68d 100644 --- a/main_http.go +++ b/main_http.go @@ -16,6 +16,7 @@ import ( 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" @@ -28,25 +29,25 @@ func main() { ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, os.Kill) svc, _ := service.NewService(ctx) - echologrus.Logger = svc.GetLogger() + echologrus.Logger = logger.Logger e := echo.New() //register shared routes - httpSvc := http.NewHttpService(svc, svc.GetLogger(), svc.GetDB(), svc.GetEventPublisher()) + httpSvc := http.NewHttpService(svc, svc.GetDB(), 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 { - svc.GetLogger().Fatalf("shutting down the server: %v", err) + logger.Logger.Fatalf("shutting down the server: %v", err) } }() //handle graceful shutdown <-ctx.Done() - svc.GetLogger().Infof("Shutting down echo server...") + logger.Logger.Infof("Shutting down echo server...") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() e.Shutdown(ctx) - svc.GetLogger().Info("Echo server exited") + logger.Logger.Info("Echo server exited") svc.WaitShutdown() - svc.GetLogger().Info("Service exited") + logger.Logger.Info("Service exited") } diff --git a/main_wails.go b/main_wails.go index 7e4d6ed78..6dc9fa0a8 100644 --- a/main_wails.go +++ b/main_wails.go @@ -7,6 +7,7 @@ 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" @@ -27,11 +28,11 @@ func main() { app := wails.NewApp(svc) wails.LaunchWailsApp(app, assets, appIcon) - svc.GetLogger().Info("Wails app exited") + logger.Logger.Info("Wails app exited") - svc.GetLogger().Info("Cancelling service context...") + logger.Logger.Info("Cancelling service context...") // cancel the service context cancel() svc.WaitShutdown() - svc.GetLogger().Info("Service exited") + logger.Logger.Info("Service exited") } diff --git a/migrations/202403171120_delete_ldk_payments.go b/migrations/202403171120_delete_ldk_payments.go index 2f8c85a9f..b723752ef 100644 --- a/migrations/202403171120_delete_ldk_payments.go +++ b/migrations/202403171120_delete_ldk_payments.go @@ -9,6 +9,7 @@ import ( "database/sql" "github.com/getAlby/nostr-wallet-connect/config" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/go-gormigrate/gormigrate/v2" "github.com/sirupsen/logrus" "gorm.io/gorm" @@ -16,14 +17,14 @@ import ( // 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 { +func _202403171120_delete_ldk_payments(appConfig *config.AppConfig) *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") + logger.Logger.Info("No LDK database, skipping migration") return nil } ldkDb, err := sql.Open("sqlite", ldkDbPath) @@ -38,7 +39,7 @@ func _202403171120_delete_ldk_payments(appConfig *config.AppConfig, logger *logr if err != nil { return err } - logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "rowsAffected": rowsAffected, }).Info("Removed incompatible payments from LDK database") diff --git a/migrations/migrate.go b/migrations/migrate.go index 276567e62..b0d8ea7ca 100644 --- a/migrations/migrate.go +++ b/migrations/migrate.go @@ -3,15 +3,14 @@ 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 { +func Migrate(db *gorm.DB, appConfig *config.AppConfig) error { m := gormigrate.New(db, gormigrate.DefaultOptions, []*gormigrate.Migration{ _202401191539_initial_migration, - _202403171120_delete_ldk_payments(appConfig, logger), + _202403171120_delete_ldk_payments(appConfig), _202404021909_nullable_expires_at, _202405302121_store_decrypted_request, _202406061259_delete_content, diff --git a/nip47/event_handler.go b/nip47/event_handler.go index 192bea65d..296286183 100644 --- a/nip47/event_handler.go +++ b/nip47/event_handler.go @@ -9,6 +9,7 @@ import ( "time" "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip04" "github.com/sirupsen/logrus" @@ -17,14 +18,14 @@ import ( func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscription, event *nostr.Event) { var nip47Response *Response - svc.logger.WithFields(logrus.Fields{ + logger.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{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, }).Errorf("Failed to compute shared secret: %v", err) @@ -36,12 +37,12 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio err = svc.db.Create(&requestEvent).Error if err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, }).Warn("Event already processed") return } - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, }).Errorf("Failed to save nostr event: %v", err) @@ -53,7 +54,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio } resp, err := svc.CreateResponse(event, nip47Response, nostr.Tags{}, ss) if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, }).Errorf("Failed to process event: %v", err) @@ -67,7 +68,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio NostrPubkey: event.PubKey, }).Error if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "nostrPubkey": event.PubKey, }).Errorf("Failed to find app for nostr pubkey: %v", err) @@ -79,7 +80,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio } resp, err := svc.CreateResponse(event, nip47Response, nostr.Tags{}, ss) if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, }).Errorf("Failed to process event: %v", err) @@ -89,7 +90,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR err = svc.db.Save(&requestEvent).Error if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "nostrPubkey": event.PubKey, }).Errorf("Failed to save state to nostr event: %v", err) } @@ -99,7 +100,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio requestEvent.AppId = &app.ID err = svc.db.Save(&requestEvent).Error if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "nostrPubkey": event.PubKey, }).Errorf("Failed to save app to nostr event: %v", err) @@ -111,7 +112,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio } resp, err := svc.CreateResponse(event, nip47Response, nostr.Tags{}, ss) if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, }).Errorf("Failed to process event: %v", err) @@ -121,7 +122,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR err = svc.db.Save(&requestEvent).Error if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "nostrPubkey": event.PubKey, }).Errorf("Failed to save state to nostr event: %v", err) } @@ -129,7 +130,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio return } - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, "appId": app.ID, @@ -138,7 +139,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio //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{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, }).Errorf("Failed to process event: %v", err) @@ -146,7 +147,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR err = svc.db.Save(&requestEvent).Error if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "nostrPubkey": event.PubKey, }).Errorf("Failed to save state to nostr event: %v", err) } @@ -155,12 +156,12 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio } payload, err := nip04.Decrypt(event.Content, ss) if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.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{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, }).Errorf("Failed to process event: %v", err) @@ -168,7 +169,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR err = svc.db.Save(&requestEvent).Error if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "nostrPubkey": event.PubKey, }).Errorf("Failed to save state to nostr event: %v", err) } @@ -178,7 +179,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio nip47Request := &Request{} err = json.Unmarshal([]byte(payload), nip47Request) if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, }).Errorf("Failed to process event: %v", err) @@ -186,7 +187,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR err = svc.db.Save(&requestEvent).Error if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "nostrPubkey": event.PubKey, }).Errorf("Failed to save state to nostr event: %v", err) } @@ -203,7 +204,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio publishResponse := func(nip47Response *Response, tags nostr.Tags) { resp, err := svc.CreateResponse(event, nip47Response, tags, ss) if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, }).Errorf("Failed to create response: %v", err) @@ -211,7 +212,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio } else { err = svc.PublishResponseEvent(ctx, sub, &requestEvent, resp, &app) if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, }).Errorf("Failed to publish event: %v", err) @@ -222,7 +223,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio } err = svc.db.Save(&requestEvent).Error if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "nostrPubkey": event.PubKey, }).Errorf("Failed to save state to nostr event: %v", err) } @@ -311,7 +312,7 @@ func (svc *nip47Service) GetMethods(app *db.App) []string { func (svc *nip47Service) decodeNip47Request(nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, methodParams interface{}) *Response { err := json.Unmarshal(nip47Request.Params, methodParams) if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, }).Errorf("Failed to decode nostr event: %v", err) @@ -333,7 +334,7 @@ func (svc *nip47Service) PublishResponseEvent(ctx context.Context, sub *nostr.Su 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{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": appId, "replyEventId": resp.ID, @@ -344,7 +345,7 @@ func (svc *nip47Service) PublishResponseEvent(ctx context.Context, sub *nostr.Su err = sub.Relay.Publish(ctx, *resp) if err != nil { responseEvent.State = db.RESPONSE_EVENT_STATE_PUBLISH_FAILED - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventId": requestEvent.ID, "requestNostrEventId": requestEvent.NostrId, "appId": appId, @@ -354,7 +355,7 @@ func (svc *nip47Service) PublishResponseEvent(ctx context.Context, sub *nostr.Su } else { responseEvent.State = db.RESPONSE_EVENT_STATE_PUBLISH_CONFIRMED responseEvent.RepliedAt = time.Now() - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventId": requestEvent.ID, "requestNostrEventId": requestEvent.NostrId, "appId": appId, @@ -365,7 +366,7 @@ func (svc *nip47Service) PublishResponseEvent(ctx context.Context, sub *nostr.Su err = svc.db.Save(&responseEvent).Error if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventId": requestEvent.ID, "requestNostrEventId": requestEvent.NostrId, "appId": appId, diff --git a/nip47/handle_balance_request.go b/nip47/handle_balance_request.go index 17ff24820..9b2ee634f 100644 --- a/nip47/handle_balance_request.go +++ b/nip47/handle_balance_request.go @@ -4,6 +4,7 @@ import ( "context" "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/nbd-wtf/go-nostr" "github.com/sirupsen/logrus" ) @@ -20,14 +21,14 @@ func (svc *nip47Service) HandleGetBalanceEvent(ctx context.Context, nip47Request return } - svc.logger.WithFields(logrus.Fields{ + logger.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{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, }).Infof("Failed to fetch balance: %v", err) diff --git a/nip47/handle_info_request.go b/nip47/handle_info_request.go index 4fe999db7..fc4f79f45 100644 --- a/nip47/handle_info_request.go +++ b/nip47/handle_info_request.go @@ -4,6 +4,7 @@ import ( "context" "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/nbd-wtf/go-nostr" "github.com/sirupsen/logrus" ) @@ -16,14 +17,14 @@ func (svc *nip47Service) HandleGetInfoEvent(ctx context.Context, nip47Request *R return } - svc.logger.WithFields(logrus.Fields{ + logger.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{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, }).Infof("Failed to fetch node info: %v", err) diff --git a/nip47/handle_list_transactions_request.go b/nip47/handle_list_transactions_request.go index 305fbd73d..85ab36e85 100644 --- a/nip47/handle_list_transactions_request.go +++ b/nip47/handle_list_transactions_request.go @@ -4,6 +4,7 @@ import ( "context" "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/nbd-wtf/go-nostr" "github.com/sirupsen/logrus" ) @@ -23,7 +24,7 @@ func (svc *nip47Service) HandleListTransactionsEvent(ctx context.Context, nip47R return } - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ // TODO: log request fields from listParams "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, @@ -37,7 +38,7 @@ func (svc *nip47Service) HandleListTransactionsEvent(ctx context.Context, nip47R } 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{ + logger.Logger.WithFields(logrus.Fields{ // TODO: log request fields from listParams "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, diff --git a/nip47/handle_lookup_invoice_request.go b/nip47/handle_lookup_invoice_request.go index 38e354de9..dafb3b813 100644 --- a/nip47/handle_lookup_invoice_request.go +++ b/nip47/handle_lookup_invoice_request.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/nbd-wtf/go-nostr" decodepay "github.com/nbd-wtf/ln-decodepay" "github.com/sirupsen/logrus" @@ -26,7 +27,7 @@ func (svc *nip47Service) HandleLookupInvoiceEvent(ctx context.Context, nip47Requ return } - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "invoice": lookupInvoiceParams.Invoice, @@ -38,7 +39,7 @@ func (svc *nip47Service) HandleLookupInvoiceEvent(ctx context.Context, nip47Requ if paymentHash == "" { paymentRequest, err := decodepay.Decodepay(strings.ToLower(lookupInvoiceParams.Invoice)) if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "invoice": lookupInvoiceParams.Invoice, @@ -58,7 +59,7 @@ func (svc *nip47Service) HandleLookupInvoiceEvent(ctx context.Context, nip47Requ transaction, err := svc.lnClient.LookupInvoice(ctx, paymentHash) if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "invoice": lookupInvoiceParams.Invoice, diff --git a/nip47/handle_make_invoice_request.go b/nip47/handle_make_invoice_request.go index c61b04723..0a95606fe 100644 --- a/nip47/handle_make_invoice_request.go +++ b/nip47/handle_make_invoice_request.go @@ -4,6 +4,7 @@ import ( "context" "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/nbd-wtf/go-nostr" "github.com/sirupsen/logrus" ) @@ -23,7 +24,7 @@ func (svc *nip47Service) HandleMakeInvoiceEvent(ctx context.Context, nip47Reques return } - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "amount": makeInvoiceParams.Amount, @@ -39,7 +40,7 @@ func (svc *nip47Service) HandleMakeInvoiceEvent(ctx context.Context, nip47Reques transaction, err := svc.lnClient.MakeInvoice(ctx, makeInvoiceParams.Amount, makeInvoiceParams.Description, makeInvoiceParams.DescriptionHash, expiry) if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "amount": makeInvoiceParams.Amount, diff --git a/nip47/handle_multi_pay_invoice_request.go b/nip47/handle_multi_pay_invoice_request.go index 3f22d28a3..025b881ff 100644 --- a/nip47/handle_multi_pay_invoice_request.go +++ b/nip47/handle_multi_pay_invoice_request.go @@ -8,6 +8,7 @@ import ( "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/events" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/nbd-wtf/go-nostr" decodepay "github.com/nbd-wtf/ln-decodepay" "github.com/sirupsen/logrus" @@ -35,7 +36,7 @@ func (svc *nip47Service) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Re bolt11 = strings.ToLower(bolt11) paymentRequest, err := decodepay.Decodepay(bolt11) if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "bolt11": bolt11, @@ -70,7 +71,7 @@ func (svc *nip47Service) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Re insertPaymentResult := svc.db.Create(&payment) mu.Unlock() if insertPaymentResult.Error != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "paymentRequest": bolt11, "invoiceId": invoiceInfo.Id, @@ -78,7 +79,7 @@ func (svc *nip47Service) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Re return } - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "bolt11": bolt11, @@ -86,7 +87,7 @@ func (svc *nip47Service) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Re response, err := svc.lnClient.SendPaymentSync(ctx, bolt11) if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "bolt11": bolt11, diff --git a/nip47/handle_multi_pay_keysend_request.go b/nip47/handle_multi_pay_keysend_request.go index 79d54c40e..ad4ef3892 100644 --- a/nip47/handle_multi_pay_keysend_request.go +++ b/nip47/handle_multi_pay_keysend_request.go @@ -6,6 +6,7 @@ import ( "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/events" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/nbd-wtf/go-nostr" "github.com/sirupsen/logrus" ) @@ -43,7 +44,7 @@ func (svc *nip47Service) HandleMultiPayKeysendEvent(ctx context.Context, nip47Re insertPaymentResult := svc.db.Create(&payment) mu.Unlock() if insertPaymentResult.Error != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "recipientPubkey": keysendInfo.Pubkey, "keysendId": keysendInfo.Id, @@ -51,7 +52,7 @@ func (svc *nip47Service) HandleMultiPayKeysendEvent(ctx context.Context, nip47Re return } - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "recipientPubkey": keysendInfo.Pubkey, @@ -59,7 +60,7 @@ func (svc *nip47Service) HandleMultiPayKeysendEvent(ctx context.Context, nip47Re preimage, err := svc.lnClient.SendKeysend(ctx, keysendInfo.Amount, keysendInfo.Pubkey, keysendInfo.Preimage, keysendInfo.TLVRecords) if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "recipientPubkey": keysendInfo.Pubkey, diff --git a/nip47/handle_pay_keysend_request.go b/nip47/handle_pay_keysend_request.go index b48504d7c..86989bbca 100644 --- a/nip47/handle_pay_keysend_request.go +++ b/nip47/handle_pay_keysend_request.go @@ -5,6 +5,7 @@ import ( "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/events" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/nbd-wtf/go-nostr" "github.com/sirupsen/logrus" ) @@ -37,7 +38,7 @@ func (svc *nip47Service) HandlePayKeysendEvent(ctx context.Context, nip47Request return } - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "senderPubkey": payParams.Pubkey, @@ -45,7 +46,7 @@ func (svc *nip47Service) HandlePayKeysendEvent(ctx context.Context, nip47Request preimage, err := svc.lnClient.SendKeysend(ctx, payParams.Amount, payParams.Pubkey, payParams.Preimage, payParams.TLVRecords) if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "recipientPubkey": payParams.Pubkey, diff --git a/nip47/handle_payment_request.go b/nip47/handle_payment_request.go index b228d12e4..84b96666b 100644 --- a/nip47/handle_payment_request.go +++ b/nip47/handle_payment_request.go @@ -7,6 +7,7 @@ import ( "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/events" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/nbd-wtf/go-nostr" decodepay "github.com/nbd-wtf/ln-decodepay" "github.com/sirupsen/logrus" @@ -26,7 +27,7 @@ func (svc *nip47Service) HandlePayInvoiceEvent(ctx context.Context, nip47Request bolt11 = strings.ToLower(bolt11) paymentRequest, err := decodepay.Decodepay(bolt11) if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "bolt11": bolt11, @@ -61,7 +62,7 @@ func (svc *nip47Service) HandlePayInvoiceEvent(ctx context.Context, nip47Request return } - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "bolt11": bolt11, @@ -69,7 +70,7 @@ func (svc *nip47Service) HandlePayInvoiceEvent(ctx context.Context, nip47Request response, err := svc.lnClient.SendPaymentSync(ctx, bolt11) if err != nil { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, "bolt11": bolt11, diff --git a/nip47/handle_sign_message_request.go b/nip47/handle_sign_message_request.go index aadb0c348..d5a7f80ce 100644 --- a/nip47/handle_sign_message_request.go +++ b/nip47/handle_sign_message_request.go @@ -4,6 +4,7 @@ import ( "context" "github.com/getAlby/nostr-wallet-connect/db" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/nbd-wtf/go-nostr" "github.com/sirupsen/logrus" ) @@ -22,14 +23,14 @@ func (svc *nip47Service) HandleSignMessageEvent(ctx context.Context, nip47Reques return } - svc.logger.WithFields(logrus.Fields{ + logger.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{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": requestEvent.NostrId, "appId": app.ID, }).Infof("Failed to sign message: %v", err) diff --git a/nip47/nip47_notification_queue.go b/nip47/nip47_notification_queue.go index 41ce84f4d..5ef96d474 100644 --- a/nip47/nip47_notification_queue.go +++ b/nip47/nip47_notification_queue.go @@ -5,7 +5,7 @@ import ( "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/nip47_notifier.go b/nip47/nip47_notifier.go index f2113a6ff..2749221db 100644 --- a/nip47/nip47_notifier.go +++ b/nip47/nip47_notifier.go @@ -9,6 +9,7 @@ import ( "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/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip04" "github.com/sirupsen/logrus" @@ -23,16 +24,14 @@ type Nip47Notifier struct { db *gorm.DB nip47Svc *nip47Service relay Relay - logger *logrus.Logger cfg config.Config lnClient lnclient.LNClient } -func NewNip47Notifier(nip47Svc *nip47Service, relay Relay, logger *logrus.Logger, cfg config.Config, db *gorm.DB, lnClient lnclient.LNClient) *Nip47Notifier { +func NewNip47Notifier(nip47Svc *nip47Service, relay Relay, cfg config.Config, db *gorm.DB, lnClient lnclient.LNClient) *Nip47Notifier { return &Nip47Notifier{ nip47Svc: nip47Svc, relay: relay, - logger: logger, cfg: cfg, db: db, lnClient: lnClient, @@ -46,13 +45,13 @@ func (notifier *Nip47Notifier) ConsumeEvent(ctx context.Context, event *events.E paymentReceivedEventProperties, ok := event.Properties.(*events.PaymentReceivedEventProperties) if !ok { - notifier.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.lnClient.LookupInvoice(ctx, paymentReceivedEventProperties.PaymentHash) if err != nil { - notifier.logger. + logger.Logger. WithField("paymentHash", paymentReceivedEventProperties.PaymentHash). WithError(err). Error("Failed to lookup invoice by payment hash") @@ -82,14 +81,14 @@ func (notifier *Nip47Notifier) notifySubscribers(ctx context.Context, notificati } func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App, notification *Notification, tags nostr.Tags) { - notifier.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).Info("Notifying subscriber") ss, err := nip04.ComputeSharedSecret(app.NostrPubkey, notifier.cfg.GetNostrSecretKey()) if err != nil { - notifier.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).WithError(err).Error("Failed to compute shared secret") @@ -98,7 +97,7 @@ func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App payloadBytes, err := json.Marshal(notification) if err != nil { - notifier.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).WithError(err).Error("Failed to stringify notification") @@ -106,7 +105,7 @@ func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App } msg, err := nip04.Encrypt(string(payloadBytes), ss) if err != nil { - notifier.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).WithError(err).Error("Failed to encrypt notification payload") @@ -125,7 +124,7 @@ func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App } err = event.Sign(notifier.cfg.GetNostrSecretKey()) if err != nil { - notifier.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).WithError(err).Error("Failed to sign event") @@ -134,13 +133,13 @@ func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App err = notifier.relay.Publish(ctx, *event) if err != nil { - notifier.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).WithError(err).Error("Failed to publish notification") return } - notifier.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "notification": notification, "appId": app.ID, }).Info("Published notification event") diff --git a/nip47/nip47_service.go b/nip47/nip47_service.go index 0309bd21f..ff46b23d5 100644 --- a/nip47/nip47_service.go +++ b/nip47/nip47_service.go @@ -7,26 +7,23 @@ import ( "github.com/getAlby/nostr-wallet-connect/events" "github.com/getAlby/nostr-wallet-connect/lnclient" "github.com/nbd-wtf/go-nostr" - "github.com/sirupsen/logrus" "gorm.io/gorm" ) type nip47Service struct { db *gorm.DB - logger *logrus.Logger nip47NotificationQueue Nip47NotificationQueue eventPublisher events.EventPublisher cfg config.Config lnClient lnclient.LNClient } -func NewNip47Service(db *gorm.DB, logger *logrus.Logger, eventPublisher events.EventPublisher, cfg config.Config, lnClient lnclient.LNClient) *nip47Service { - nip47NotificationQueue := NewNip47NotificationQueue(logger) +func NewNip47Service(db *gorm.DB, eventPublisher events.EventPublisher, cfg config.Config, lnClient lnclient.LNClient) *nip47Service { + nip47NotificationQueue := NewNip47NotificationQueue() eventPublisher.RegisterSubscriber(nip47NotificationQueue) return &nip47Service{ nip47NotificationQueue: nip47NotificationQueue, db: db, - logger: logger, eventPublisher: eventPublisher, cfg: cfg, lnClient: lnClient, @@ -38,7 +35,7 @@ func (svc *nip47Service) Stop() { } func (svc *nip47Service) StartNotifier(ctx context.Context, relay *nostr.Relay, lnClient lnclient.LNClient) { - nip47Notifier := NewNip47Notifier(svc, relay, svc.logger, svc.cfg, svc.db, lnClient) + nip47Notifier := NewNip47Notifier(svc, relay, svc.cfg, svc.db, lnClient) go func() { for { select { diff --git a/nip47/permissions.go b/nip47/permissions.go index 8b7c68934..6d3a693e6 100644 --- a/nip47/permissions.go +++ b/nip47/permissions.go @@ -6,6 +6,7 @@ import ( "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/events" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/sirupsen/logrus" ) @@ -26,7 +27,7 @@ func (svc *nip47Service) HasPermission(app *db.App, requestMethod string, amount } expiresAt := appPermission.ExpiresAt if expiresAt != nil && expiresAt.Before(time.Now()) { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestMethod": requestMethod, "expiresAt": expiresAt.Unix(), "appId": app.ID, @@ -52,7 +53,7 @@ func (svc *nip47Service) HasPermission(app *db.App, requestMethod string, amount func (svc *nip47Service) checkPermission(nip47Request *Request, requestNostrEventId string, app *db.App, amount int64) *Response { hasPermission, code, message := svc.HasPermission(app, nip47Request.Method, amount) if !hasPermission { - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": requestNostrEventId, "appId": app.ID, "code": code, diff --git a/service/models.go b/service/models.go index 298bacde9..6034e5cea 100644 --- a/service/models.go +++ b/service/models.go @@ -6,7 +6,6 @@ import ( "github.com/getAlby/nostr-wallet-connect/events" "github.com/getAlby/nostr-wallet-connect/lnclient" "github.com/getAlby/nostr-wallet-connect/nip47" - "github.com/sirupsen/logrus" "gorm.io/gorm" ) @@ -17,10 +16,8 @@ type Service interface { StopApp() StopLNClient() error StopDb() error - GetLogFilePath() string GetAlbyOAuthSvc() alby.AlbyOAuthService GetNip47Service() nip47.Nip47Service - GetLogger() *logrus.Logger GetDB() *gorm.DB WaitShutdown() GetEventPublisher() events.EventPublisher diff --git a/service/service.go b/service/service.go index 218b2c4bd..a5e771159 100644 --- a/service/service.go +++ b/service/service.go @@ -10,23 +10,21 @@ import ( "os" "path" "path/filepath" - "strconv" "strings" "sync" "github.com/adrg/xdg" "github.com/nbd-wtf/go-nostr" - "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/logger" "github.com/getAlby/nostr-wallet-connect/config" "github.com/getAlby/nostr-wallet-connect/db" @@ -41,16 +39,10 @@ import ( "github.com/getAlby/nostr-wallet-connect/nip47" ) -const ( - logDir = "log" - logFilename = "nwc.log" -) - type service struct { cfg config.Config db *gorm.DB lnClient lnclient.LNClient - logger *logrus.Logger albyOAuthSvc alby.AlbyOAuthService eventPublisher events.EventPublisher ctx context.Context @@ -68,38 +60,21 @@ func NewService(ctx context.Context) (*service, error) { 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)) + logger.Init(appConfig.LogLevel) if appConfig.Workdir == "" { appConfig.Workdir = filepath.Join(xdg.DataHome, "/alby-nwc") - logger.WithField("workdir", appConfig.Workdir).Info("No workdir specified, using default") + logger.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, - ) + err = logger.AddFileLogger(appConfig.Workdir) if err != nil { return nil, err } - logger.AddHook(fileLoggerHook) - finishRestoreNode(logger, appConfig.Workdir) + finishRestoreNode(appConfig.Workdir) // If DATABASE_URI is a URI or a path, leave it unchanged. // If it only contains a filename, prepend the workdir. @@ -130,18 +105,18 @@ func NewService(ctx context.Context) (*service, error) { } sqlDb.SetMaxOpenConns(1) - err = migrations.Migrate(gormDB, appConfig, logger) + err = migrations.Migrate(gormDB, appConfig) if err != nil { - logger.WithError(err).Error("Failed to migrate") + logger.Logger.WithError(err).Error("Failed to migrate") return nil, err } - cfg := config.NewConfig(gormDB, appConfig, logger) + cfg := config.NewConfig(gormDB, appConfig) - eventPublisher := events.NewEventPublisher(logger) + eventPublisher := events.NewEventPublisher() if err != nil { - logger.WithError(err).Error("Failed to create Alby OAuth service") + logger.Logger.WithError(err).Error("Failed to create Alby OAuth service") return nil, err } @@ -151,9 +126,8 @@ func NewService(ctx context.Context) (*service, error) { db: gormDB, ctx: ctx, wg: &wg, - logger: logger, eventPublisher: eventPublisher, - albyOAuthSvc: alby.NewAlbyOAuthService(logger, cfg, cfg.GetEnv(), db.NewDBService(gormDB, logger)), + albyOAuthSvc: alby.NewAlbyOAuthService(cfg, cfg.GetEnv(), db.NewDBService(gormDB)), } eventPublisher.RegisterSubscriber(svc.albyOAuthSvc) @@ -175,10 +149,10 @@ func NewService(ctx context.Context) (*service, error) { func (svc *service) StopLNClient() error { if svc.lnClient != nil { - svc.logger.Info("Shutting down LDK client") + logger.Logger.Info("Shutting down LDK client") err := svc.lnClient.Shutdown() if err != nil { - svc.logger.WithError(err).Error("Failed to stop LN backend") + logger.Logger.WithError(err).Error("Failed to stop LN backend") svc.eventPublisher.Publish(&events.Event{ Event: "nwc_node_stop_failed", Properties: map[string]interface{}{ @@ -187,7 +161,7 @@ func (svc *service) StopLNClient() error { }) return err } - svc.logger.Info("Publishing node shutdown event") + logger.Logger.Info("Publishing node shutdown event") svc.lnClient = nil svc.eventPublisher.Publish(&events.Event{ Event: "nwc_node_stopped", @@ -195,7 +169,7 @@ func (svc *service) StopLNClient() error { // TODO: move this, use contexts instead svc.nip47Service.Stop() } - svc.logger.Info("LNClient stopped successfully") + logger.Logger.Info("LNClient stopped successfully") return nil } @@ -210,53 +184,53 @@ func (svc *service) launchLNBackend(ctx context.Context, encryptionKey string) e return errors.New("no LNBackendType specified") } - svc.logger.Infof("Launching LN Backend: %s", lnBackend) + 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, svc.logger, LNDAddress, LNDCertHex, LNDMacaroonHex) + 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.logger, svc.cfg, svc.eventPublisher, Mnemonic, LDKWorkdir, svc.cfg.GetEnv().LDKNetwork, svc.cfg.GetEnv().LDKEsploraServer, svc.cfg.GetEnv().LDKGossipSource) + 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, svc.logger, Mnemonic, GreenlightInviteCode, GreenlightWorkdir, encryptionKey) + 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(svc.logger, Mnemonic, BreezAPIKey, GreenlightInviteCode, BreezWorkdir) + 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(svc.logger, PhoenixdAddress, PhoenixdAuthorization) + 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(svc.logger, cashuWorkdir, cashuMintUrl) + lnClient, err = cashu.NewCashuService(cashuWorkdir, cashuMintUrl) default: - svc.logger.Fatalf("Unsupported LNBackendType: %v", lnBackend) + logger.Logger.Fatalf("Unsupported LNBackendType: %v", lnBackend) } if err != nil { - svc.logger.WithError(err).Error("Failed to launch LN backend") + logger.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") + logger.Logger.WithError(err).Error("Failed to fetch node info") } if info != nil && info.Pubkey != "" { svc.eventPublisher.SetGlobalProperty("node_id", info.Pubkey) @@ -281,7 +255,7 @@ func (svc *service) createFilters(identityPubkey string) nostr.Filters { } func (svc *service) noticeHandler(notice string) { - svc.logger.Infof("Received a notice %s", notice) + logger.Logger.Infof("Received a notice %s", notice) } func (svc *service) GetLNClient() lnclient.LNClient { @@ -294,65 +268,61 @@ func (svc *service) StartSubscription(ctx context.Context, sub *nostr.Subscripti go func() { // block till EOS is received <-sub.EndOfStoredEvents - svc.logger.Info("Received EOS") + logger.Logger.Info("Received EOS") // loop through incoming events for event := range sub.Events { go svc.nip47Service.HandleEvent(ctx, sub, event) } - svc.logger.Info("Relay subscription events channel ended") + logger.Logger.Info("Relay subscription events channel ended") }() <-ctx.Done() if sub.Relay.ConnectionError != nil { - svc.logger.WithField("connectionError", sub.Relay.ConnectionError).Error("Relay error") + logger.Logger.WithField("connectionError", sub.Relay.ConnectionError).Error("Relay error") return sub.Relay.ConnectionError } - svc.logger.Info("Exiting subscription...") + logger.Logger.Info("Exiting subscription...") return nil } -func (svc *service) GetLogFilePath() string { - return filepath.Join(svc.cfg.GetEnv().Workdir, logDir, logFilename) -} - -func finishRestoreNode(logger *logrus.Logger, workDir string) { +func finishRestoreNode(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") + logger.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") + 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.WithField("filename", file.Name()).WithError(err).Fatal("Failed to remove file") + logger.Logger.WithField("filename", file.Name()).WithError(err).Fatal("Failed to remove file") } - logger.WithField("filename", file.Name()).Info("removed file") + logger.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") + 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.WithField("filename", file.Name()).WithError(err).Fatal("Failed to move file") + logger.Logger.WithField("filename", file.Name()).WithError(err).Fatal("Failed to move file") } - logger.WithField("filename", file.Name()).Info("copied file from restore directory") + logger.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.Logger.WithError(err).Fatal("Failed to remove restore directory") } - logger.WithField("restoreDir", restoreDir).Info("removed restore directory") + logger.Logger.WithField("restoreDir", restoreDir).Info("removed restore directory") } } @@ -437,15 +407,11 @@ func (svc *service) GetDB() *gorm.DB { return svc.db } -func (svc *service) GetLogger() *logrus.Logger { - return svc.logger -} - func (svc *service) GetEventPublisher() events.EventPublisher { return svc.eventPublisher } func (svc *service) WaitShutdown() { - svc.logger.Info("Waiting for service to exit...") + logger.Logger.Info("Waiting for service to exit...") svc.wg.Wait() } diff --git a/service/start.go b/service/start.go index 6d7479b6a..c42389d9d 100644 --- a/service/start.go +++ b/service/start.go @@ -10,26 +10,27 @@ import ( "github.com/sirupsen/logrus" "github.com/getAlby/nostr-wallet-connect/events" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/getAlby/nostr-wallet-connect/nip47" ) func (svc *service) StartNostr(ctx context.Context, encryptionKey string) error { - svc.nip47Service = nip47.NewNip47Service(svc.db, svc.logger, svc.eventPublisher, svc.cfg, svc.lnClient) + svc.nip47Service = nip47.NewNip47Service(svc.db, svc.eventPublisher, svc.cfg, svc.lnClient) relayUrl := svc.cfg.GetRelayUrl() err := svc.cfg.Start(encryptionKey) if err != nil { - svc.logger.WithError(err).Fatal("Failed to start config") + logger.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") + logger.Logger.WithError(err).Fatal("Error converting nostr privkey to pubkey") } - svc.logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "npub": npub, "hex": svc.cfg.GetNostrPublicKey(), }).Info("Starting nostr-wallet-connect") @@ -43,11 +44,11 @@ func (svc *service) StartNostr(ctx context.Context, encryptionKey string) error if i > 0 { sleepDuration := 10 contextCancelled := false - svc.logger.Infof("[Iteration %d] Retrying in %d seconds...", i, sleepDuration) + logger.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") + logger.Logger.Info("service context cancelled while waiting for retry") contextCancelled = true case <-time.After(time.Duration(sleepDuration) * time.Second): //timeout } @@ -58,49 +59,49 @@ func (svc *service) StartNostr(ctx context.Context, encryptionKey string) error if relay != nil && relay.IsConnected() { err := relay.Close() if err != nil { - svc.logger.WithError(err).Error("Could not close relay connection") + logger.Logger.WithError(err).Error("Could not close relay connection") } } //connect to the relay - svc.logger.Infof("Connecting to the relay: %s", relayUrl) + logger.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") + 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 { - svc.logger.WithError(err).Error("Could not publish NIP47 info") + logger.Logger.WithError(err).Error("Could not publish NIP47 info") } - svc.logger.Info("Subscribing to events") + logger.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") + 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. - svc.logger.WithError(err).Error("Got an error from the relay while listening to subscription.") + 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 } - svc.logger.Info("Disconnecting from relay...") + logger.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") + logger.Logger.WithError(err).Error("Could not close relay connection") } } svc.Shutdown() - svc.logger.Info("Relay subroutine ended") + logger.Logger.Info("Relay subroutine ended") svc.wg.Done() }() return nil @@ -108,7 +109,7 @@ func (svc *service) StartNostr(ctx context.Context, encryptionKey string) error func (svc *service) StartApp(encryptionKey string) error { if !svc.cfg.CheckUnlockPassword(encryptionKey) { - svc.logger.Errorf("Invalid password") + logger.Logger.Errorf("Invalid password") return errors.New("invalid password") } @@ -116,7 +117,7 @@ func (svc *service) StartApp(encryptionKey string) error { err := svc.launchLNBackend(ctx, encryptionKey) if err != nil { - svc.logger.Errorf("Failed to launch LN backend: %v", err) + logger.Logger.Errorf("Failed to launch LN backend: %v", err) svc.eventPublisher.Publish(&events.Event{ Event: "nwc_node_start_failed", }) diff --git a/tests/create_test_service.go b/tests/create_test_service.go index f403bc038..12cead554 100644 --- a/tests/create_test_service.go +++ b/tests/create_test_service.go @@ -6,6 +6,7 @@ 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/migrations" "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/glebarez/sqlite" @@ -26,16 +27,16 @@ func createTestService() (svc *TestService, err error) { return nil, err } - logger := logrus.New() - logger.SetFormatter(&logrus.JSONFormatter{}) - logger.SetOutput(os.Stdout) - logger.SetLevel(logrus.InfoLevel) + logger.Logger = logrus.New() + logger.Logger.SetFormatter(&logrus.JSONFormatter{}) + logger.Logger.SetOutput(os.Stdout) + logger.Logger.SetLevel(logrus.InfoLevel) appConfig := &config.AppConfig{ Workdir: ".test", } - err = migrations.Migrate(gormDb, appConfig, logger) + err = migrations.Migrate(gormDb, appConfig) if err != nil { return nil, err } @@ -43,20 +44,18 @@ func createTestService() (svc *TestService, err error) { cfg := config.NewConfig( gormDb, appConfig, - logger, ) cfg.Start("") - eventPublisher := events.NewEventPublisher(logger) + eventPublisher := events.NewEventPublisher() return &TestService{ cfg: cfg, db: gormDb, lnClient: mockLn, - logger: logger, eventPublisher: eventPublisher, - nip47Svc: nip47.NewNip47Service(gormDb, logger, eventPublisher, cfg, mockLn), + nip47Svc: nip47.NewNip47Service(gormDb, eventPublisher, cfg, mockLn), }, nil } @@ -64,7 +63,6 @@ type TestService struct { cfg config.Config db *gorm.DB lnClient lnclient.LNClient - logger *logrus.Logger eventPublisher events.EventPublisher nip47Svc nip47.Nip47Service } diff --git a/tests/nip47_notifier_test.go b/tests/nip47_notifier_test.go index 8c6b3772c..f2c5078a3 100644 --- a/tests/nip47_notifier_test.go +++ b/tests/nip47_notifier_test.go @@ -36,7 +36,7 @@ func TestSendNotification(t *testing.T) { err = svc.db.Create(appPermission).Error assert.NoError(t, err) - nip47NotificationQueue := nip47.NewNip47NotificationQueue(svc.logger) + nip47NotificationQueue := nip47.NewNip47NotificationQueue() svc.eventPublisher.RegisterSubscriber(nip47NotificationQueue) testEvent := &events.Event{ @@ -55,9 +55,9 @@ func TestSendNotification(t *testing.T) { relay := NewMockRelay() - nip47Svc := nip47.NewNip47Service(svc.db, svc.logger, svc.eventPublisher, svc.cfg, svc.lnClient) + nip47Svc := nip47.NewNip47Service(svc.db, svc.eventPublisher, svc.cfg, svc.lnClient) - notifier := nip47.NewNip47Notifier(nip47Svc, relay, svc.logger, svc.cfg, svc.db, svc.lnClient) + notifier := nip47.NewNip47Notifier(nip47Svc, relay, svc.cfg, svc.db, svc.lnClient) notifier.ConsumeEvent(ctx, receivedEvent) assert.NotNil(t, relay.publishedEvent) @@ -93,7 +93,7 @@ func TestSendNotificationNoPermission(t *testing.T) { _, _, err = createApp(svc) assert.NoError(t, err) - nip47NotificationQueue := nip47.NewNip47NotificationQueue(svc.logger) + nip47NotificationQueue := nip47.NewNip47NotificationQueue() svc.eventPublisher.RegisterSubscriber(nip47NotificationQueue) testEvent := &events.Event{ @@ -112,9 +112,9 @@ func TestSendNotificationNoPermission(t *testing.T) { relay := NewMockRelay() - nip47Svc := nip47.NewNip47Service(svc.db, svc.logger, svc.eventPublisher, svc.cfg, svc.lnClient) + nip47Svc := nip47.NewNip47Service(svc.db, svc.eventPublisher, svc.cfg, svc.lnClient) - notifier := nip47.NewNip47Notifier(nip47Svc, relay, svc.logger, svc.cfg, svc.db, svc.lnClient) + notifier := nip47.NewNip47Notifier(nip47Svc, relay, svc.cfg, svc.db, svc.lnClient) notifier.ConsumeEvent(ctx, receivedEvent) assert.Nil(t, relay.publishedEvent) diff --git a/wails/wails_app.go b/wails/wails_app.go index b90c0a945..1da838f18 100644 --- a/wails/wails_app.go +++ b/wails/wails_app.go @@ -24,7 +24,7 @@ type WailsApp struct { func NewApp(svc service.Service) *WailsApp { return &WailsApp{ svc: svc, - api: api.NewAPI(svc, svc.GetLogger(), svc.GetDB(), svc.GetConfig()), + api: api.NewAPI(svc, svc.GetDB(), svc.GetConfig()), } } @@ -35,7 +35,7 @@ func (app *WailsApp) startup(ctx context.Context) { } func LaunchWailsApp(app *WailsApp, assets embed.FS, appIcon []byte) { - logger := NewWailsLogger(app.svc.GetLogger()) + logger := NewWailsLogger(app.logger.Logger) err := wails.Run(&options.App{ Title: "AlbyHub", @@ -45,7 +45,6 @@ func LaunchWailsApp(app *WailsApp, assets embed.FS, appIcon []byte) { 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, @@ -75,7 +74,7 @@ func NewWailsLogger(appLogger *logrus.Logger) WailsLogger { } type WailsLogger struct { - AppLogger *logrus.Logger + App } func (logger WailsLogger) Print(message string) { @@ -91,15 +90,15 @@ func (logger WailsLogger) Debug(message string) { } func (logger WailsLogger) Info(message string) { - logger.AppLogger.Info(message) + logger.Applogger.Logger.Info(message) } func (logger WailsLogger) Warning(message string) { - logger.AppLogger.Warning(message) + logger.Applogger.Logger.Warning(message) } func (logger WailsLogger) Error(message string) { - logger.AppLogger.Error(message) + logger.Applogger.Logger.Error(message) } func (logger WailsLogger) Fatal(message string) { diff --git a/wails/wails_handlers.go b/wails/wails_handlers.go index 62b3d04d2..a320a86aa 100644 --- a/wails/wails_handlers.go +++ b/wails/wails_handlers.go @@ -37,7 +37,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string err := app.svc.GetAlbyOAuthSvc().CallbackHandler(ctx, code) if err != nil { - app.svc.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -72,7 +72,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string updateAppRequest := &api.UpdateAppRequest{} err := json.Unmarshal([]byte(body), updateAppRequest) if err != nil { - app.svc.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -169,7 +169,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string case "/api/alby/me": me, err := app.svc.GetAlbyOAuthSvc().GetMe(ctx) if err != nil { - app.svc.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -180,7 +180,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string case "/api/alby/balance": balance, err := app.svc.GetAlbyOAuthSvc().GetBalance(ctx) if err != nil { - app.svc.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -194,7 +194,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string payRequest := &alby.AlbyPayRequest{} err := json.Unmarshal([]byte(body), payRequest) if err != nil { - app.svc.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -218,7 +218,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.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -235,7 +235,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.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -269,7 +269,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.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -316,7 +316,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.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -358,7 +358,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.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -406,7 +406,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.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -416,7 +416,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string err = app.api.SetNextBackupReminder(backupReminderRequest) if err != nil { - app.svc.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -428,7 +428,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.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -438,7 +438,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string err = app.api.ChangeUnlockPassword(changeUnlockPasswordRequest) if err != nil { - app.svc.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -450,7 +450,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.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -459,7 +459,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string } err = app.api.Start(startRequest) if err != nil { - app.svc.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -473,7 +473,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.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -482,7 +482,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string } err = app.api.Setup(ctx, setupRequest) if err != nil { - app.svc.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -494,7 +494,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.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -503,7 +503,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string } sendPaymentProbesResponse, err := app.api.SendPaymentProbes(ctx, sendPaymentProbesRequest) if err != nil { - app.svc.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -515,7 +515,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.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -524,7 +524,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string } sendSpontaneousPaymentProbesResponse, err := app.api.SendSpontaneousPaymentProbes(ctx, sendSpontaneousPaymentProbesRequest) if err != nil { - app.svc.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -536,7 +536,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.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -549,7 +549,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string DefaultFilename: "nwc.bkp", }) if err != nil { - app.svc.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -559,7 +559,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string backupFile, err := os.Create(saveFilePath) if err != nil { - app.svc.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -572,7 +572,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string err = app.api.CreateBackup(backupRequest.UnlockPassword, backupFile) if err != nil { - app.svc.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -584,7 +584,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.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -597,7 +597,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string DefaultFilename: "nwc.bkp", }) if err != nil { - app.svc.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -607,7 +607,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string backupFile, err := os.Open(backupFilePath) if err != nil { - app.svc.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -619,7 +619,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string err = app.api.RestoreBackup(restoreRequest.UnlockPassword, backupFile) if err != nil { - app.svc.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -637,7 +637,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.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -646,7 +646,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string } logOutputResponse, err := app.api.GetLogOutput(ctx, logType, getLogOutputRequest) if err != nil { - app.svc.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -656,7 +656,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string return WailsRequestRouterResponse{Body: logOutputResponse, Error: ""} } - app.svc.GetLogger().WithFields(logrus.Fields{ + app.logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, }).Error("Unhandled route") From 080421b53300cb650fd58064092b52528d4c5dd8 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Sun, 16 Jun 2024 13:52:19 +0700 Subject: [PATCH 04/16] chore: change nip-47 handlers into controllers - further split nip-47 package - move tests into related packages - move migrations into separate package in db folder - renaming - fix some logger usages --- README.md | 2 +- alby/alby_oauth_service.go | 73 +- api/api.go | 36 +- api/backup.go | 25 +- api/lsp.go | 32 +- api/models.go | 8 +- config/config.go | 12 +- db/db.go | 54 + db/db_service.go | 4 +- .../202401191539_initial_migration.go | 0 .../202401191539_initial_migration.sql | 0 .../202403171120_delete_ldk_payments.go | 44 + .../202404021909_nullable_expires_at.go | 0 .../202405302121_store_decrypted_request.go | 0 .../202406061259_delete_content.go | 0 .../migrations}/202406071726_vacuum.go | 0 {migrations => db/migrations}/README.md | 0 {migrations => db/migrations}/migrate.go | 7 +- db/models.go | 2 +- http/http_service.go | 30 +- lnclient/breez/breez.go | 19 +- lnclient/cashu/cashu.go | 15 +- lnclient/greenlight/greenlight.go | 20 +- lnclient/ldk/ldk.go | 21 +- lnclient/lnd/lnd.go | 18 +- lnclient/models.go | 2 +- lnclient/phoenixd/phoenixd.go | 19 +- main_http.go | 2 +- .../202403171120_delete_ldk_payments.go | 52 - nip47/budgets.go | 40 - nip47/controllers/controller_test.go | 796 +++++++++++++++ nip47/controllers/decode_request.go | 25 + nip47/controllers/get_balance_controller.go | 78 ++ nip47/controllers/get_info_controller.go | 85 ++ .../list_transactions_controller.go | 87 ++ .../controllers/lookup_invoice_controller.go | 104 ++ nip47/controllers/make_invoice_controller.go | 89 ++ nip47/controllers/models.go | 14 + .../multi_pay_invoice_controller.go | 91 ++ .../multi_pay_invoice_controller_test.go | 199 ++++ .../multi_pay_keysend_controller.go | 64 ++ nip47/controllers/pay_invoice_controller.go | 138 +++ nip47/controllers/pay_keysend_controller.go | 112 ++ nip47/controllers/sign_message_controller.go | 75 ++ nip47/event_handler.go | 248 +++-- .../event_handler_test.go | 42 +- nip47/handle_balance_request.go | 62 -- nip47/handle_info_request.go | 62 -- nip47/handle_list_transactions_request.go | 65 -- nip47/handle_lookup_invoice_request.go | 87 -- nip47/handle_make_invoice_request.go | 70 -- nip47/handle_multi_pay_invoice_request.go | 139 --- nip47/handle_multi_pay_keysend_request.go | 109 -- nip47/handle_pay_keysend_request.go | 86 -- nip47/handle_payment_request.go | 114 --- nip47/handle_sign_message_request.go | 56 - nip47/models.go | 198 ---- nip47/models/models.go | 64 ++ nip47/nip47_service.go | 29 +- nip47/notifications/models.go | 17 + .../nip47_notification_queue.go | 2 +- nip47/{ => notifications}/nip47_notifier.go | 30 +- nip47/notifications/nip47_notifier_test.go | 136 +++ nip47/permissions.go | 83 -- nip47/permissions/permissions.go | 128 +++ .../permissions/permissions_test.go | 59 +- nip47/publish_nip47_info.go | 8 +- service/models.go | 13 +- service/service.go | 65 +- service/start.go | 3 - tests/create_app.go | 6 +- tests/create_test_service.go | 72 -- tests/mock_ln_client.go | 41 +- tests/nip47_event_handlers_test.go | 962 ------------------ tests/nip47_notifier_test.go | 135 --- tests/test_service.go | 59 ++ wails/wails_app.go | 45 +- wails/wails_handlers.go | 78 +- 78 files changed, 2977 insertions(+), 2890 deletions(-) create mode 100644 db/db.go rename {migrations => db/migrations}/202401191539_initial_migration.go (100%) rename {migrations => db/migrations}/202401191539_initial_migration.sql (100%) create mode 100644 db/migrations/202403171120_delete_ldk_payments.go rename {migrations => db/migrations}/202404021909_nullable_expires_at.go (100%) rename {migrations => db/migrations}/202405302121_store_decrypted_request.go (100%) rename {migrations => db/migrations}/202406061259_delete_content.go (100%) rename {migrations => db/migrations}/202406071726_vacuum.go (100%) rename {migrations => db/migrations}/README.md (100%) rename {migrations => db/migrations}/migrate.go (54%) delete mode 100644 migrations/202403171120_delete_ldk_payments.go delete mode 100644 nip47/budgets.go create mode 100644 nip47/controllers/controller_test.go create mode 100644 nip47/controllers/decode_request.go create mode 100644 nip47/controllers/get_balance_controller.go create mode 100644 nip47/controllers/get_info_controller.go create mode 100644 nip47/controllers/list_transactions_controller.go create mode 100644 nip47/controllers/lookup_invoice_controller.go create mode 100644 nip47/controllers/make_invoice_controller.go create mode 100644 nip47/controllers/models.go create mode 100644 nip47/controllers/multi_pay_invoice_controller.go create mode 100644 nip47/controllers/multi_pay_invoice_controller_test.go create mode 100644 nip47/controllers/multi_pay_keysend_controller.go create mode 100644 nip47/controllers/pay_invoice_controller.go create mode 100644 nip47/controllers/pay_keysend_controller.go create mode 100644 nip47/controllers/sign_message_controller.go rename tests/nip47_event_handler_test.go => nip47/event_handler_test.go (51%) delete mode 100644 nip47/handle_balance_request.go delete mode 100644 nip47/handle_info_request.go delete mode 100644 nip47/handle_list_transactions_request.go delete mode 100644 nip47/handle_lookup_invoice_request.go delete mode 100644 nip47/handle_make_invoice_request.go delete mode 100644 nip47/handle_multi_pay_invoice_request.go delete mode 100644 nip47/handle_multi_pay_keysend_request.go delete mode 100644 nip47/handle_pay_keysend_request.go delete mode 100644 nip47/handle_payment_request.go delete mode 100644 nip47/handle_sign_message_request.go delete mode 100644 nip47/models.go create mode 100644 nip47/models/models.go create mode 100644 nip47/notifications/models.go rename nip47/{ => notifications}/nip47_notification_queue.go (97%) rename nip47/{ => notifications}/nip47_notifier.go (83%) create mode 100644 nip47/notifications/nip47_notifier_test.go delete mode 100644 nip47/permissions.go create mode 100644 nip47/permissions/permissions.go rename tests/nip47_permissions_test.go => nip47/permissions/permissions_test.go (50%) delete mode 100644 tests/create_test_service.go delete mode 100644 tests/nip47_event_handlers_test.go delete mode 100644 tests/nip47_notifier_test.go create mode 100644 tests/test_service.go diff --git a/README.md b/README.md index 42cf2e6a4..77906b5c3 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ _If you get a blank screen, try running in your normal terminal (outside of vsco ### Testing - $ go test ./tests + $ go test ./... ### Profiling diff --git a/alby/alby_oauth_service.go b/alby/alby_oauth_service.go index 00c24ba70..4d8db38c5 100644 --- a/alby/alby_oauth_service.go +++ b/alby/alby_oauth_service.go @@ -14,19 +14,19 @@ 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/logger" - "github.com/getAlby/nostr-wallet-connect/nip47" + nip47 "github.com/getAlby/nostr-wallet-connect/nip47/models" ) type albyOAuthService struct { - appConfig *config.AppConfig - config config.Config + cfg config.Config oauthConf *oauth2.Config - dbSvc db.DBService + db *gorm.DB } const ( @@ -36,29 +36,28 @@ const ( userIdentifierKey = "AlbyUserIdentifier" ) -func NewAlbyOAuthService(config config.Config, appConfig *config.AppConfig, dbSvc db.DBService) *albyOAuthService { +func NewAlbyOAuthService(db *gorm.DB, cfg config.Config) *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, - dbSvc: dbSvc, + cfg: cfg, + db: db, } return albyOAuthSvc } @@ -75,7 +74,7 @@ func (svc *albyOAuthService) CallbackHandler(ctx context.Context, code string) e if err != nil { 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 } @@ -87,10 +86,10 @@ func (svc *albyOAuthService) CallbackHandler(ctx context.Context, code string) e // 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.") } @@ -98,7 +97,7 @@ 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 { logger.Logger.WithError(err).Error("Failed to fetch user identifier from user configs") return "", err @@ -115,9 +114,9 @@ func (svc *albyOAuthService) IsConnected(ctx context.Context) bool { } 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 @@ -125,7 +124,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 } @@ -134,7 +133,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 } @@ -147,7 +146,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 } @@ -186,7 +185,7 @@ func (svc *albyOAuthService) GetMe(ctx context.Context) (*AlbyMe, error) { 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 { logger.Logger.WithError(err).Error("Error creating request /me") return nil, err @@ -221,7 +220,7 @@ func (svc *albyOAuthService) GetBalance(ctx context.Context) (*AlbyBalance, erro 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 { logger.Logger.WithError(err).Error("Error creating request to balance endpoint") return nil, err @@ -269,7 +268,7 @@ func (svc *albyOAuthService) SendPayment(ctx context.Context, invoice string) er 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 { logger.Logger.WithError(err).Error("Error creating request bolt11 endpoint") return err @@ -331,7 +330,7 @@ func (svc *albyOAuthService) SendPayment(ctx context.Context, invoice string) er } func (svc *albyOAuthService) GetAuthUrl() string { - if svc.appConfig.AlbyClientId == "" || svc.appConfig.AlbyClientSecret == "" { + 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") @@ -344,7 +343,7 @@ func (svc *albyOAuthService) LinkAccount(ctx context.Context) error { return err } - app, _, err := svc.dbSvc.CreateApp( + app, _, err := db.NewDBService(svc.db).CreateApp( "getalby.com", connectionPubkey, 1_000_000, @@ -373,7 +372,7 @@ 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 { + if !svc.cfg.GetEnv().LogEvents { logger.Logger.WithField("event", event).Debug("Skipped sending to alby events API") return nil } @@ -436,7 +435,7 @@ func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Eve 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 { logger.Logger.WithError(err).Error("Error creating request /events") return err @@ -489,7 +488,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) } @@ -508,7 +507,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) } @@ -541,7 +540,7 @@ func (svc *albyOAuthService) createAlbyAccountNWCNode(ctx context.Context) (stri } createNodeRequest := createNWCNodeRequest{ - WalletPubkey: svc.config.GetNostrPublicKey(), + WalletPubkey: svc.cfg.GetNostrPublicKey(), } body := bytes.NewBuffer([]byte{}) @@ -552,7 +551,7 @@ func (svc *albyOAuthService) createAlbyAccountNWCNode(ctx context.Context) (stri 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 { logger.Logger.WithError(err).Error("Error creating request /internal/nwcs") return "", err @@ -603,7 +602,7 @@ func (svc *albyOAuthService) activateAlbyAccountNWCNode(ctx context.Context) err 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 { logger.Logger.WithError(err).Error("Error creating request /internal/nwcs/activate") return err @@ -640,7 +639,7 @@ func (svc *albyOAuthService) GetChannelPeerSuggestions(ctx context.Context) ([]C 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 { logger.Logger.WithError(err).Error("Error creating request to channel_suggestions endpoint") return nil, err diff --git a/api/api.go b/api/api.go index 6c54b4f6c..06b83cb1a 100644 --- a/api/api.go +++ b/api/api.go @@ -17,26 +17,30 @@ 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/logger" - "github.com/getAlby/nostr-wallet-connect/nip47" + 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/utils" ) type api struct { - db *gorm.DB - dbSvc db.DBService - cfg config.Config - svc service.Service + db *gorm.DB + dbSvc db.DBService + cfg config.Config + svc service.Service + permissionsSvc permissions.PermissionsService } -func NewAPI(svc service.Service, gormDb *gorm.DB, config config.Config) *api { +func NewAPI(svc service.Service, gormDB *gorm.DB, config config.Config, eventsPublisher events.EventPublisher) *api { return &api{ - db: gormDb, - dbSvc: db.NewDBService(gormDb), - cfg: config, - svc: svc, + db: gormDB, + dbSvc: db.NewDBService(gormDB), + cfg: config, + svc: svc, + permissionsSvc: permissions.NewPermissionsService(gormDB, eventsPublisher), } } @@ -138,7 +142,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 { @@ -187,10 +191,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.GetNip47Service().GetBudgetUsage(&paySpecificPermission) + budgetUsage = api.permissionsSvc.GetBudgetUsage(&paySpecificPermission) } response := App{ @@ -243,9 +247,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.GetNip47Service().GetBudgetUsage(&permission) + apiApp.BudgetUsage = api.permissionsSvc.GetBudgetUsage(&permission) } } } diff --git a/api/backup.go b/api/backup.go index 65e81feb1..9365416b6 100644 --- a/api/backup.go +++ b/api/backup.go @@ -17,46 +17,47 @@ import ( "crypto/rand" "crypto/sha256" + "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/logger" "golang.org/x/crypto/pbkdf2" ) -func (bs *api) 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.svc.GetConfig().CheckUnlockPassword(unlockPassword) { return errors.New("invalid unlock password") } - workDir, err := filepath.Abs(bs.svc.GetConfig().GetEnv().Workdir) + workDir, err := filepath.Abs(api.svc.GetConfig().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) } 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 { 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 { logger.Logger.WithError(err).Error("Failed to stop database") return fmt.Errorf("failed to close database: %w", err) @@ -104,7 +105,7 @@ func (bs *api) CreateBackup(unlockPassword string, w io.Writer) error { } // Locate the main database file. - dbFilePath := bs.svc.GetConfig().GetEnv().DatabaseUri + dbFilePath := api.svc.GetConfig().GetEnv().DatabaseUri // Add the database file to the archive. logger.Logger.WithField("nwc.db", dbFilePath).Info("adding nwc db to zip") err = addFileToZip(dbFilePath, "nwc.db") @@ -132,13 +133,13 @@ func (bs *api) CreateBackup(unlockPassword string, w io.Writer) error { return nil } -func (bs *api) 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.svc.GetConfig().GetEnv().Workdir) if err != nil { return fmt.Errorf("failed to get absolute workdir: %w", err) } - if strings.HasPrefix(bs.svc.GetConfig().GetEnv().DatabaseUri, "file:") { + if strings.HasPrefix(api.svc.GetConfig().GetEnv().DatabaseUri, "file:") { return errors.New("cannot restore backup when database path is a file URI") } diff --git a/api/lsp.go b/api/lsp.go index a1dd411d4..d129e1718 100644 --- a/api/lsp.go +++ b/api/lsp.go @@ -25,7 +25,7 @@ type lspConnectionInfo struct { Port uint16 } -func (ls *api) NewInstantChannelInvoice(ctx context.Context, request *NewInstantChannelInvoiceRequest) (*NewInstantChannelInvoiceResponse, error) { +func (api *api) NewInstantChannelInvoice(ctx context.Context, request *NewInstantChannelInvoiceRequest) (*NewInstantChannelInvoiceResponse, error) { var selectedLsp lsp.LSP switch request.LSP { case "VOLTAGE": @@ -48,7 +48,7 @@ func (ls *api) NewInstantChannelInvoice(ctx context.Context, request *NewInstant return nil, errors.New("unknown LSP") } - if ls.svc.GetLNClient() == nil { + if api.svc.GetLNClient() == nil { return nil, errors.New("LNClient not started") } @@ -64,10 +64,10 @@ func (ls *api) NewInstantChannelInvoice(ctx context.Context, request *NewInstant case lsp.LSP_TYPE_FLOW_2_0: fallthrough case lsp.LSP_TYPE_PMLSP: - lspInfo, err = ls.getFlowLSPInfo(selectedLsp.Url + "/info") + lspInfo, err = api.getFlowLSPInfo(selectedLsp.Url + "/info") case lsp.LSP_TYPE_LSPS1: - lspInfo, err = ls.getLSPS1LSPInfo(selectedLsp.Url + "/get_info") + lspInfo, err = api.getLSPS1LSPInfo(selectedLsp.Url + "/get_info") default: return nil, fmt.Errorf("unsupported LSP type: %v", selectedLsp.LspType) @@ -79,7 +79,7 @@ func (ls *api) NewInstantChannelInvoice(ctx context.Context, request *NewInstant logger.Logger.Infoln("Requesting own node info") - nodeInfo, err := ls.svc.GetLNClient().GetInfo(ctx) + nodeInfo, err := api.svc.GetLNClient().GetInfo(ctx) if err != nil { logger.Logger.WithError(err).WithFields(logrus.Fields{ "url": selectedLsp.Url, @@ -89,7 +89,7 @@ func (ls *api) NewInstantChannelInvoice(ctx context.Context, request *NewInstant 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, @@ -105,11 +105,11 @@ func (ls *api) NewInstantChannelInvoice(ctx context.Context, request *NewInstant switch selectedLsp.LspType { case lsp.LSP_TYPE_FLOW_2_0: - invoice, fee, err = ls.requestFlow20WrappedInvoice(ctx, &selectedLsp, request.Amount, nodeInfo.Pubkey) + invoice, fee, err = api.requestFlow20WrappedInvoice(ctx, &selectedLsp, request.Amount, nodeInfo.Pubkey) case lsp.LSP_TYPE_PMLSP: - invoice, fee, err = ls.requestPMLSPInvoice(&selectedLsp, request.Amount, nodeInfo.Pubkey) + invoice, fee, err = api.requestPMLSPInvoice(&selectedLsp, request.Amount, nodeInfo.Pubkey) case lsp.LSP_TYPE_LSPS1: - invoice, fee, err = ls.requestLSPS1Invoice(ctx, &selectedLsp, request.Amount, nodeInfo.Pubkey, request.Public) + invoice, fee, err = api.requestLSPS1Invoice(ctx, &selectedLsp, request.Amount, nodeInfo.Pubkey, request.Public) default: return nil, fmt.Errorf("unsupported LSP type: %v", selectedLsp.LspType) @@ -131,7 +131,7 @@ func (ls *api) NewInstantChannelInvoice(ctx context.Context, request *NewInstant return newChannelResponse, nil } -func (ls *api) getLSPS1LSPInfo(url string) (*lspConnectionInfo, error) { +func (api *api) getLSPS1LSPInfo(url string) (*lspConnectionInfo, error) { type LSPS1LSPInfo struct { // TODO: implement options Options interface{} `json:"options"` @@ -199,7 +199,7 @@ func (ls *api) getLSPS1LSPInfo(url string) (*lspConnectionInfo, error) { Port: uint16(port), }, nil } -func (ls *api) getFlowLSPInfo(url string) (*lspConnectionInfo, error) { +func (api *api) getFlowLSPInfo(url string) (*lspConnectionInfo, error) { type FlowLSPConnectionMethod struct { Address string `json:"address"` Port uint16 `json:"port"` @@ -267,7 +267,7 @@ func (ls *api) getFlowLSPInfo(url string) (*lspConnectionInfo, error) { }, nil } -func (ls *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *lsp.LSP, amount uint64, pubkey string) (invoice string, fee uint64, err error) { +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 { @@ -352,7 +352,7 @@ func (ls *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *lsp // 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 { logger.Logger.WithError(err).Error("Failed to request own invoice") return "", 0, fmt.Errorf("failed to request own invoice %v", err) @@ -439,7 +439,7 @@ func (ls *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *lsp return invoice, fee, nil } -func (ls *api) requestPMLSPInvoice(selectedLsp *lsp.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"` @@ -514,7 +514,7 @@ func (ls *api) requestPMLSPInvoice(selectedLsp *lsp.LSP, amount uint64, pubkey s return invoice, fee, nil } -func (ls *api) requestLSPS1Invoice(ctx context.Context, selectedLsp *lsp.LSP, amount uint64, pubkey string, public bool) (invoice string, fee uint64, err error) { +func (api *api) requestLSPS1Invoice(ctx context.Context, selectedLsp *lsp.LSP, amount uint64, pubkey string, public bool) (invoice string, fee uint64, err error) { client := http.Client{ Timeout: time.Second * 10, } @@ -531,7 +531,7 @@ func (ls *api) requestLSPS1Invoice(ctx context.Context, selectedLsp *lsp.LSP, am AnnounceChannel bool `json:"announce_channel"` } - refundAddress, err := ls.svc.GetLNClient().GetNewOnchainAddress(ctx) + refundAddress, err := api.svc.GetLNClient().GetNewOnchainAddress(ctx) if err != nil { logger.Logger.WithError(err).Error("Failed to request onchain address") return "", 0, err diff --git a/api/models.go b/api/models.go index d7663ccbc..f42a6126c 100644 --- a/api/models.go +++ b/api/models.go @@ -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"` diff --git a/config/config.go b/config/config.go index 599f644e0..8712763dd 100644 --- a/config/config.go +++ b/config/config.go @@ -26,14 +26,15 @@ const ( unlockPasswordCheck = "THIS STRING SHOULD MATCH IF PASSWORD IS CORRECT" ) -func NewConfig(db *gorm.DB, env *AppConfig) *config { - cfg := &config{} - cfg.init(db, env) +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) { - cfg.db = db +func (cfg *config) init(env *AppConfig) { cfg.Env = env if cfg.Env.Relay != "" { @@ -215,6 +216,7 @@ func (cfg *config) Setup(encryptionKey string) { cfg.SetUpdate("UnlockPasswordCheck", unlockPasswordCheck, encryptionKey) } +// TODO: move Nostr out of config func (cfg *config) Start(encryptionKey string) error { nostrSecretKey, _ := cfg.Get("NostrSecretKey", encryptionKey) 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 074becf73..136895dc5 100644 --- a/db/db_service.go +++ b/db/db_service.go @@ -20,7 +20,7 @@ func NewDBService(db *gorm.DB) *dbService { } } -func (svc *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 == "" { @@ -50,7 +50,7 @@ func (svc *dbService) CreateApp(name string, pubkey string, maxAmount int, budge RequestMethod: m, ExpiresAt: expiresAt, //these fields are only relevant for pay_invoice - MaxAmount: maxAmount, + MaxAmount: int(maxAmount), BudgetRenewal: budgetRenewal, } err = tx.Create(&appPermission).Error 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..00daae3a4 --- /dev/null +++ b/db/migrations/202403171120_delete_ldk_payments.go @@ -0,0 +1,44 @@ +package migrations + +import ( + _ "embed" + + "github.com/go-gormigrate/gormigrate/v2" + "gorm.io/gorm" +) + +// 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/migrations/migrate.go b/db/migrations/migrate.go similarity index 54% rename from migrations/migrate.go rename to db/migrations/migrate.go index b0d8ea7ca..cdd3f6b97 100644 --- a/migrations/migrate.go +++ b/db/migrations/migrate.go @@ -1,16 +1,15 @@ package migrations import ( - "github.com/getAlby/nostr-wallet-connect/config" "github.com/go-gormigrate/gormigrate/v2" "gorm.io/gorm" ) -func Migrate(db *gorm.DB, appConfig *config.AppConfig) error { +func Migrate(gormDB *gorm.DB) error { - m := gormigrate.New(db, gormigrate.DefaultOptions, []*gormigrate.Migration{ + m := gormigrate.New(gormDB, gormigrate.DefaultOptions, []*gormigrate.Migration{ _202401191539_initial_migration, - _202403171120_delete_ldk_payments(appConfig), + _202403171120_delete_ldk_payments, _202404021909_nullable_expires_at, _202405302121_store_decrypted_request, _202406061259_delete_content, 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/http/http_service.go b/http/http_service.go index 3910ec7c6..237ddcd31 100644 --- a/http/http_service.go +++ b/http/http_service.go @@ -29,8 +29,8 @@ type HttpService struct { api api.API albyHttpSvc *alby.AlbyHttpService cfg config.Config - db *gorm.DB eventPublisher events.EventPublisher + db *gorm.DB } const ( @@ -38,13 +38,13 @@ const ( sessionCookieAuthKey = "authenticated" ) -func NewHttpService(svc service.Service, db *gorm.DB, eventPublisher events.EventPublisher) *HttpService { +func NewHttpService(svc service.Service, eventPublisher events.EventPublisher) *HttpService { return &HttpService{ - api: api.NewAPI(svc, db, svc.GetConfig()), + api: api.NewAPI(svc, svc.GetDB(), svc.GetConfig(), svc.GetEventPublisher()), albyHttpSvc: alby.NewAlbyHttpService(svc.GetAlbyOAuthSvc(), svc.GetConfig().GetEnv()), cfg: svc.GetConfig(), - db: db, eventPublisher: eventPublisher, + db: svc.GetDB(), } } @@ -601,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{ @@ -610,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) } @@ -623,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{ @@ -632,7 +635,7 @@ func (httpSvc *HttpService) appsUpdateHandler(c echo.Context) error { }) } - err := httpSvc.api.UpdateApp(&app, &requestData) + err := httpSvc.api.UpdateApp(&dbApp, &requestData) if err != nil { logger.Logger.WithError(err).Error("Failed to update app") @@ -651,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{ @@ -664,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", }) diff --git a/lnclient/breez/breez.go b/lnclient/breez/breez.go index c864f7c0e..cbe1401f1 100644 --- a/lnclient/breez/breez.go +++ b/lnclient/breez/breez.go @@ -18,7 +18,6 @@ import ( "github.com/getAlby/nostr-wallet-connect/lnclient" "github.com/getAlby/nostr-wallet-connect/logger" - "github.com/getAlby/nostr-wallet-connect/nip47" ) type BreezService struct { @@ -117,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{ @@ -128,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) @@ -150,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 @@ -163,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), @@ -188,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 { @@ -206,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 { @@ -231,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 @@ -277,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) @@ -309,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, diff --git a/lnclient/cashu/cashu.go b/lnclient/cashu/cashu.go index 47236fa54..d2905a014 100644 --- a/lnclient/cashu/cashu.go +++ b/lnclient/cashu/cashu.go @@ -13,7 +13,6 @@ import ( "github.com/elnosh/gonuts/wallet/storage" "github.com/getAlby/nostr-wallet-connect/lnclient" "github.com/getAlby/nostr-wallet-connect/logger" - "github.com/getAlby/nostr-wallet-connect/nip47" decodepay "github.com/nbd-wtf/ln-decodepay" "github.com/sirupsen/logrus" ) @@ -90,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") } @@ -105,7 +104,7 @@ 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 { logger.Logger.WithError(err).Error("Failed to mint") @@ -123,7 +122,7 @@ 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 { @@ -138,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() @@ -274,7 +273,7 @@ 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 { logger.Logger.WithFields(logrus.Fields{ @@ -300,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, diff --git a/lnclient/greenlight/greenlight.go b/lnclient/greenlight/greenlight.go index 60610b618..b21f30daa 100644 --- a/lnclient/greenlight/greenlight.go +++ b/lnclient/greenlight/greenlight.go @@ -22,7 +22,6 @@ import ( "github.com/getAlby/nostr-wallet-connect/config" "github.com/getAlby/nostr-wallet-connect/lnclient" "github.com/getAlby/nostr-wallet-connect/logger" - "github.com/getAlby/nostr-wallet-connect/nip47" ) type GreenlightService struct { @@ -127,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{} @@ -138,11 +137,10 @@ 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, }) @@ -172,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{ @@ -198,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, @@ -212,7 +210,7 @@ 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, }) @@ -241,7 +239,7 @@ func (gs *GreenlightService) LookupInvoice(ctx context.Context, paymentHash stri 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 { @@ -249,7 +247,7 @@ func (gs *GreenlightService) ListTransactions(ctx context.Context, from, until, return nil, err } - transactions = []nip47.Transaction{} + transactions = []lnclient.Transaction{} if err != nil { log.Printf("ListInvoices failed: %v", err) @@ -543,7 +541,7 @@ func (gs *GreenlightService) SignMessage(ctx context.Context, message string) (s 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 { @@ -581,7 +579,7 @@ func (gs *GreenlightService) greenlightInvoiceToTransaction(invoice *glalby.List settledAt = &paidAt } - transaction := &nip47.Transaction{ + transaction := &lnclient.Transaction{ Type: "incoming", Invoice: bolt11, Description: description, diff --git a/lnclient/ldk/ldk.go b/lnclient/ldk/ldk.go index e54f69d1a..ecdc1b46c 100644 --- a/lnclient/ldk/ldk.go +++ b/lnclient/ldk/ldk.go @@ -27,7 +27,6 @@ import ( "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" ) @@ -493,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{} @@ -507,7 +506,7 @@ 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 { logger.Logger.WithError(err).Error("Keysend failed") return "", err @@ -627,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() @@ -667,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, @@ -681,7 +680,7 @@ 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 { @@ -699,8 +698,8 @@ func (ls *LDKService) LookupInvoice(ctx context.Context, paymentHash string) (tr 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() @@ -738,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{} } } @@ -949,7 +948,7 @@ func (ls *LDKService) SignMessage(ctx context.Context, message string) (string, return sign, nil } -func (ls *LDKService) ldkPaymentToTransaction(payment *ldk_node.PaymentDetails) (*nip47.Transaction, error) { +func (ls *LDKService) ldkPaymentToTransaction(payment *ldk_node.PaymentDetails) (*lnclient.Transaction, error) { // logger.Logger.WithField("payment", payment).Debug("Mapping LDK payment to transaction") transactionType := "incoming" @@ -1027,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, diff --git a/lnclient/lnd/lnd.go b/lnclient/lnd/lnd.go index f7d0efbb7..1e13f7d2c 100644 --- a/lnclient/lnd/lnd.go +++ b/lnclient/lnd/lnd.go @@ -18,7 +18,6 @@ import ( "github.com/getAlby/nostr-wallet-connect/lnclient" "github.com/getAlby/nostr-wallet-connect/lnclient/lnd/wrapper" "github.com/getAlby/nostr-wallet-connect/logger" - "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/sirupsen/logrus" // "gorm.io/gorm" @@ -30,7 +29,6 @@ import ( // todo: drop dependency on lndhub package type LNDService struct { client *wrapper.LNDWrapper - // db *gorm.DB } 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" { @@ -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,7 +243,7 @@ func (svc *LNDService) ListChannels(ctx context.Context) ([]lnclient.Channel, er return channels, nil } -func (svc *LNDService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *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 != "" { @@ -276,7 +274,7 @@ 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 { @@ -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 @@ -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, @@ -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 664f0a736..bb8c0f986 100644 --- a/lnclient/models.go +++ b/lnclient/models.go @@ -42,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 992c2bf5d..177776eb4 100644 --- a/lnclient/phoenixd/phoenixd.go +++ b/lnclient/phoenixd/phoenixd.go @@ -14,7 +14,6 @@ import ( "github.com/getAlby/nostr-wallet-connect/lnclient" "github.com/getAlby/nostr-wallet-connect/logger" - "github.com/getAlby/nostr-wallet-connect/nip47" "github.com/sirupsen/logrus" ) @@ -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)) @@ -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, @@ -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) @@ -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/main_http.go b/main_http.go index bac94e68d..29e088e8e 100644 --- a/main_http.go +++ b/main_http.go @@ -33,7 +33,7 @@ func main() { e := echo.New() //register shared routes - httpSvc := http.NewHttpService(svc, svc.GetDB(), svc.GetEventPublisher()) + httpSvc := http.NewHttpService(svc, svc.GetEventPublisher()) httpSvc.RegisterSharedRoutes(e) //start Echo server go func() { diff --git a/migrations/202403171120_delete_ldk_payments.go b/migrations/202403171120_delete_ldk_payments.go deleted file mode 100644 index b723752ef..000000000 --- a/migrations/202403171120_delete_ldk_payments.go +++ /dev/null @@ -1,52 +0,0 @@ -package migrations - -import ( - _ "embed" - "errors" - "os" - "path/filepath" - - "database/sql" - - "github.com/getAlby/nostr-wallet-connect/config" - "github.com/getAlby/nostr-wallet-connect/logger" - "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) *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.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 - }, - Rollback: func(tx *gorm.DB) error { - return nil - }, - } -} diff --git a/nip47/budgets.go b/nip47/budgets.go deleted file mode 100644 index 4fd2ddf7c..000000000 --- a/nip47/budgets.go +++ /dev/null @@ -1,40 +0,0 @@ -package nip47 - -import ( - "time" - - "github.com/getAlby/nostr-wallet-connect/db" -) - -func (svc *nip47Service) GetStartOfBudget(budget_type string, createdAt time.Time) time.Time { - now := time.Now() - switch budget_type { - case 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 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 BUDGET_RENEWAL_MONTHLY: - return time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) - case BUDGET_RENEWAL_YEARLY: - return time.Date(now.Year(), time.January, 1, 0, 0, 0, 0, now.Location()) - default: //"never" - return createdAt - } -} - -func (svc *nip47Service) 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, svc.GetStartOfBudget(appPermission.BudgetRenewal, appPermission.App.CreatedAt)).Scan(&result) - return int64(result.Sum) -} diff --git a/nip47/controllers/controller_test.go b/nip47/controllers/controller_test.go new file mode 100644 index 000000000..e652b6d5c --- /dev/null +++ b/nip47/controllers/controller_test.go @@ -0,0 +1,796 @@ +package controllers + +// TODO: split and re-enable tests + +// import ( +// "context" +// "encoding/json" +// "testing" + +// "github.com/nbd-wtf/go-nostr" +// "github.com/stretchr/testify/assert" + +// "github.com/getAlby/nostr-wallet-connect/nip47/models" +// "github.com/getAlby/nostr-wallet-connect/tests" +// ) + +// // TODO: split up this file! + +// 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 nip47PayWrongMethodJson = ` +// { +// "method": "get_balance", +// "params": { +// "invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" +// } +// } +// ` +// const nip47PayJsonNoInvoice = ` +// { +// "method": "pay_invoice", +// "params": { +// "something": "else" +// } +// } +// ` + +// func TestHandleMultiPayKeysendEvent(t *testing.T) { + +// ctx := context.TODO() +// defer tests.RemoveTestService() +// svc, err := tests.CreateTestService() +// assert.NoError(t, err) + +// app, ss, err := tests.CreateApp(svc) +// assert.NoError(t, err) + +// request := &models.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: models.REQUEST_KIND, +// PubKey: app.NostrPubkey, +// Content: payload, +// } + +// reqEvent.ID = "multi_pay_keysend_without_permission" +// requestEvent := &db.RequestEvent{ +// NostrId: reqEvent.ID, +// } + +// responses := []*models.Response{} +// dTags := []nostr.Tags{} + +// publishResponse := func(response *models.Response, tags nostr.Tags) { +// responses = append(responses, response) +// dTags = append(dTags, tags) +// } + +// svc.nip47Svc.HandleMultiPayKeysendEvent(ctx, request, requestEvent, app, publishResponse) + +// assert.Equal(t, 2, len(responses)) +// for i := 0; i < len(responses); i++ { +// assert.Equal(t, models.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 models.PAY_KEYSEND_METHOD, see +// // https://github.com/getAlby/nostr-wallet-connect/issues/189 +// 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 = "multi_pay_keysend_with_permission" +// requestEvent.NostrId = reqEvent.ID +// responses = []*models.Response{} +// dTags = []nostr.Tags{} +// svc.nip47Svc.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.(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 = []*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()) +// } + +// func TestHandleGetBalanceEvent(t *testing.T) { +// ctx := context.TODO() +// defer tests.RemoveTestService() +// svc, err := tests.CreateTestService() +// assert.NoError(t, err) + +// app, ss, err := tests.CreateApp(svc) +// assert.NoError(t, err) + +// request := &models.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: models.REQUEST_KIND, +// PubKey: app.NostrPubkey, +// Content: payload, +// } + +// reqEvent.ID = "test_get_balance_without_permission" +// requestEvent := &db.RequestEvent{ +// NostrId: reqEvent.ID, +// } + +// responses := []*models.Response{} + +// publishResponse := func(response *models.Response, tags nostr.Tags) { +// responses = append(responses, response) +// } + +// err = svc.DB.Create(&requestEvent).Error +// assert.NoError(t, err) + +// svc.nip47Svc.HandleGetBalanceEvent(ctx, request, requestEvent, app, publishResponse) + +// assert.Equal(t, responses[0].Error.Code, models.ERROR_RESTRICTED) + +// // with permission +// expiresAt := time.Now().Add(24 * time.Hour) +// appPermission := &db.AppPermission{ +// AppId: app.ID, +// App: *app, +// RequestMethod: models.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 = []*models.Response{} +// svc.nip47Svc.HandleGetBalanceEvent(ctx, request, requestEvent, app, publishResponse) + +// assert.Equal(t, responses[0].Result.(*getBalanceResponse).Balance, int64(21000)) + +// // 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, request, requestEvent, 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) +// } + +// func TestHandlePayInvoiceEvent(t *testing.T) { +// ctx := context.TODO() +// defer tests.RemoveTestService() +// svc, err := tests.CreateTestService() +// assert.NoError(t, err) + +// app, ss, err := tests.CreateApp(svc) +// assert.NoError(t, err) + +// request := &models.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: models.REQUEST_KIND, +// PubKey: app.NostrPubkey, +// Content: payload, +// } + +// reqEvent.ID = "pay_invoice_without_permission" +// requestEvent := &db.RequestEvent{ +// NostrId: reqEvent.ID, +// } + +// responses := []*models.Response{} + +// publishResponse := func(response *models.Response, tags nostr.Tags) { +// responses = append(responses, response) +// } + +// svc.nip47Svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + +// assert.Equal(t, models.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: models.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 = []*models.Response{} +// svc.nip47Svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + +// assert.Equal(t, responses[0].Result.(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 = []*models.Response{} +// svc.nip47Svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + +// assert.Equal(t, models.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 = []*models.Response{} +// svc.nip47Svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + +// assert.Equal(t, models.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 = []*models.Response{} +// svc.nip47Svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + +// assert.Equal(t, models.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 = []*models.Response{} +// svc.nip47Svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + +// assert.Equal(t, models.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 = []*models.Response{} +// svc.nip47Svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + +// assert.Equal(t, responses[0].Result.(payResponse).Preimage, "123preimage") +// } + +// func TestHandlePayKeysendEvent(t *testing.T) { +// ctx := context.TODO() +// defer tests.RemoveTestService() +// svc, err := tests.CreateTestService() +// assert.NoError(t, err) + +// app, ss, err := tests.CreateApp(svc) +// assert.NoError(t, err) + +// request := &models.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: models.REQUEST_KIND, +// PubKey: app.NostrPubkey, +// Content: payload, +// } + +// reqEvent.ID = "pay_keysend_without_permission" +// requestEvent := &db.RequestEvent{ +// NostrId: reqEvent.ID, +// } + +// responses := []*models.Response{} + +// publishResponse := func(response *models.Response, tags nostr.Tags) { +// responses = append(responses, response) +// } + +// svc.nip47Svc.HandlePayKeysendEvent(ctx, request, requestEvent, app, publishResponse) + +// assert.Equal(t, models.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 models.PAY_KEYSEND_METHOD, see +// // https://github.com/getAlby/nostr-wallet-connect/issues/189 +// 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 = "pay_keysend_with_permission" +// requestEvent.NostrId = reqEvent.ID +// responses = []*models.Response{} +// svc.nip47Svc.HandlePayKeysendEvent(ctx, request, requestEvent, app, publishResponse) + +// assert.Equal(t, responses[0].Result.(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 = []*models.Response{} +// svc.nip47Svc.HandlePayKeysendEvent(ctx, request, requestEvent, app, publishResponse) + +// assert.Equal(t, models.ERROR_QUOTA_EXCEEDED, responses[0].Error.Code) +// } + +// func TestHandleLookupInvoiceEvent(t *testing.T) { +// ctx := context.TODO() +// defer tests.RemoveTestService() +// svc, err := tests.CreateTestService() +// assert.NoError(t, err) + +// app, ss, err := tests.CreateApp(svc) +// assert.NoError(t, err) + +// request := &models.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: models.REQUEST_KIND, +// PubKey: app.NostrPubkey, +// Content: payload, +// } + +// reqEvent.ID = "test_lookup_invoice_without_permission" +// requestEvent := &db.RequestEvent{ +// NostrId: reqEvent.ID, +// } + +// responses := []*models.Response{} + +// publishResponse := func(response *models.Response, tags nostr.Tags) { +// responses = append(responses, response) +// } + +// svc.nip47Svc.HandleLookupInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + +// assert.Equal(t, models.ERROR_RESTRICTED, responses[0].Error.Code) + +// // with permission +// expiresAt := time.Now().Add(24 * time.Hour) +// appPermission := &db.AppPermission{ +// AppId: app.ID, +// App: *app, +// RequestMethod: models.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 = []*models.Response{} +// svc.nip47Svc.HandleLookupInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + +// transaction := responses[0].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) +// } + +// func TestHandleMakeInvoiceEvent(t *testing.T) { +// ctx := context.TODO() +// defer tests.RemoveTestService() +// svc, err := tests.CreateTestService() +// assert.NoError(t, err) + +// app, ss, err := tests.CreateApp(svc) +// assert.NoError(t, err) + +// request := &models.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: models.REQUEST_KIND, +// PubKey: app.NostrPubkey, +// Content: payload, +// } + +// reqEvent.ID = "test_make_invoice_without_permission" +// requestEvent := &db.RequestEvent{ +// NostrId: reqEvent.ID, +// } + +// responses := []*models.Response{} + +// publishResponse := func(response *models.Response, tags nostr.Tags) { +// responses = append(responses, response) +// } + +// svc.nip47Svc.HandleMakeInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + +// assert.Equal(t, models.ERROR_RESTRICTED, responses[0].Error.Code) + +// // with permission +// expiresAt := time.Now().Add(24 * time.Hour) +// appPermission := &db.AppPermission{ +// AppId: app.ID, +// App: *app, +// RequestMethod: models.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 = []*models.Response{} +// svc.nip47Svc.HandleMakeInvoiceEvent(ctx, request, requestEvent, app, publishResponse) + +// assert.Equal(t, tests.MockTransaction.Preimage, responses[0].Result.(*makeInvoiceResponse).Preimage) +// } + +// func TestHandleListTransactionsEvent(t *testing.T) { +// ctx := context.TODO() +// defer tests.RemoveTestService() +// svc, err := tests.CreateTestService() +// assert.NoError(t, err) + +// app, ss, err := tests.CreateApp(svc) +// assert.NoError(t, err) + +// request := &models.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: models.REQUEST_KIND, +// PubKey: app.NostrPubkey, +// Content: payload, +// } + +// reqEvent.ID = "test_list_transactions_without_permission" +// requestEvent := &db.RequestEvent{ +// NostrId: reqEvent.ID, +// } + +// responses := []*models.Response{} + +// publishResponse := func(response *models.Response, tags nostr.Tags) { +// responses = append(responses, response) +// } + +// svc.nip47Svc.HandleListTransactionsEvent(ctx, request, requestEvent, app, publishResponse) + +// assert.Equal(t, models.ERROR_RESTRICTED, responses[0].Error.Code) + +// // with permission +// expiresAt := time.Now().Add(24 * time.Hour) +// appPermission := &db.AppPermission{ +// AppId: app.ID, +// App: *app, +// RequestMethod: models.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 = []*models.Response{} +// svc.nip47Svc.HandleListTransactionsEvent(ctx, request, requestEvent, app, publishResponse) + +// assert.Equal(t, 2, len(responses[0].Result.(*listTransactionsResponse).Transactions)) +// transaction := responses[0].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) +// } + +// func TestHandleGetInfoEvent(t *testing.T) { +// ctx := context.TODO() +// defer tests.RemoveTestService() +// svc, err := tests.CreateTestService() +// assert.NoError(t, err) + +// app, ss, err := tests.CreateApp(svc) +// assert.NoError(t, err) + +// request := &models.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: models.REQUEST_KIND, +// PubKey: app.NostrPubkey, +// Content: payload, +// } + +// reqEvent.ID = "test_get_info_without_permission" +// requestEvent := &db.RequestEvent{ +// NostrId: reqEvent.ID, +// } + +// responses := []*models.Response{} + +// publishResponse := func(response *models.Response, tags nostr.Tags) { +// responses = append(responses, response) +// } + +// svc.nip47Svc.HandleGetInfoEvent(ctx, request, requestEvent, app, publishResponse) + +// assert.Equal(t, models.ERROR_RESTRICTED, responses[0].Error.Code) + +// expiresAt := time.Now().Add(24 * time.Hour) +// appPermission := &db.AppPermission{ +// AppId: app.ID, +// App: *app, +// RequestMethod: models.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 = []*models.Response{} +// svc.nip47Svc.HandleGetInfoEvent(ctx, request, requestEvent, app, publishResponse) + +// nodeInfo := responses[0].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/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..8b278bb85 --- /dev/null +++ b/nip47/controllers/get_balance_controller.go @@ -0,0 +1,78 @@ +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, + } +} + +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_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/list_transactions_controller.go b/nip47/controllers/list_transactions_controller.go new file mode 100644 index 000000000..6e1db622f --- /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/lookup_invoice_controller.go b/nip47/controllers/lookup_invoice_controller.go new file mode 100644 index 000000000..a769ee1bb --- /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/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/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..c0c0c7c7b --- /dev/null +++ b/nip47/controllers/multi_pay_invoice_controller_test.go @@ -0,0 +1,199 @@ +package controllers + +import ( + "context" + "encoding/json" + "testing" + "time" + + "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{} + + requestEvent := &db.RequestEvent{} + svc.DB.Save(requestEvent) + + 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, requestEvent.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()) + } +} + +// TODO: split up this test +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) + + // with permission + maxAmount := 1000 + budgetRenewal := "never" + expiresAt := time.Now().Add(24 * time.Hour) + 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) + + 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)) + for i := 0; i < len(responses); i++ { + assert.Equal(t, responses[i].Result.(payResponse).Preimage, "123preimage") + assert.Equal(t, tests.MockPaymentHash, dTags[i].GetFirst([]string{"d"}).Value()) + } + + // one malformed invoice + err = json.Unmarshal([]byte(nip47MultiPayOneMalformedInvoiceJson), 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) + + assert.Equal(t, 2, len(responses)) + assert.Equal(t, "invoiceId123", dTags[0].GetFirst([]string{"d"}).Value()) + assert.Equal(t, responses[0].Error.Code, models.ERROR_INTERNAL) + + assert.Equal(t, tests.MockPaymentHash, dTags[1].GetFirst([]string{"d"}).Value()) + assert.Equal(t, responses[1].Result.(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), 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/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_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/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 index 296286183..959edb5a8 100644 --- a/nip47/event_handler.go +++ b/nip47/event_handler.go @@ -5,19 +5,22 @@ import ( "encoding/json" "errors" "fmt" - "slices" "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) { - var nip47Response *Response +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, @@ -28,7 +31,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, - }).Errorf("Failed to compute shared secret: %v", err) + }).WithError(err).Error("Failed to compute shared secret") return } @@ -45,10 +48,10 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, - }).Errorf("Failed to save nostr event: %v", err) - nip47Response = &Response{ - Error: &Error{ - Code: ERROR_INTERNAL, + }).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()), }, } @@ -57,9 +60,9 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, - }).Errorf("Failed to process event: %v", err) + }).WithError(err).Error("Failed to process event") } - svc.PublishResponseEvent(ctx, sub, &requestEvent, resp, nil) + svc.publishResponseEvent(ctx, sub, &requestEvent, resp, nil) return } @@ -70,11 +73,11 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio if err != nil { logger.Logger.WithFields(logrus.Fields{ "nostrPubkey": event.PubKey, - }).Errorf("Failed to find app for nostr pubkey: %v", err) + }).WithError(err).Error("Failed to find app for nostr pubkey") - nip47Response = &Response{ - Error: &Error{ - Code: ERROR_UNAUTHORIZED, + nip47Response = &models.Response{ + Error: &models.Error{ + Code: models.ERROR_UNAUTHORIZED, Message: "The public key does not have a wallet connected.", }, } @@ -83,16 +86,16 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, - }).Errorf("Failed to process event: %v", err) + }).WithError(err).Error("Failed to process event") } - svc.PublishResponseEvent(ctx, sub, &requestEvent, resp, &app) + 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, - }).Errorf("Failed to save state to nostr event: %v", err) + }).WithError(err).Error("Failed to save state to nostr event") } return } @@ -102,11 +105,11 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio if err != nil { logger.Logger.WithFields(logrus.Fields{ "nostrPubkey": event.PubKey, - }).Errorf("Failed to save app to nostr event: %v", err) + }).WithError(err).Error("Failed to save app to nostr event") - nip47Response = &Response{ - Error: &Error{ - Code: ERROR_UNAUTHORIZED, + nip47Response = &models.Response{ + Error: &models.Error{ + Code: models.ERROR_UNAUTHORIZED, Message: fmt.Sprintf("Failed to save app to nostr event: %s", err.Error()), }, } @@ -115,16 +118,16 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, - }).Errorf("Failed to process event: %v", err) + }).WithError(err).Error("Failed to process event") } - svc.PublishResponseEvent(ctx, sub, &requestEvent, resp, &app) + 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, - }).Errorf("Failed to save state to nostr event: %v", err) + }).WithError(err).Error("Failed to save state to nostr event") } return @@ -142,14 +145,14 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, - }).Errorf("Failed to process event: %v", err) + }).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, - }).Errorf("Failed to save state to nostr event: %v", err) + }).WithError(err).Error("Failed to save state to nostr event") } return @@ -160,36 +163,36 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio "requestEventNostrId": event.ID, "eventKind": event.Kind, "appId": app.ID, - }).Errorf("Failed to decrypt content: %v", err) + }).WithError(err).Error("Failed to decrypt content") logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, - }).Errorf("Failed to process event: %v", err) + }).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, - }).Errorf("Failed to save state to nostr event: %v", err) + }).WithError(err).Error("Failed to save state to nostr event") } return } - nip47Request := &Request{} + nip47Request := &models.Request{} err = json.Unmarshal([]byte(payload), nip47Request) if err != nil { logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, - }).Errorf("Failed to process event: %v", err) + }).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, - }).Errorf("Failed to save state to nostr event: %v", err) + }).WithError(err).Error("Failed to save state to nostr event") } return @@ -200,71 +203,134 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio 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 *Response, tags nostr.Tags) { + // 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, - }).Errorf("Failed to create response: %v", err) + "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) + err = svc.publishResponseEvent(ctx, sub, &requestEvent, resp, &app) if err != nil { logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, "eventKind": event.Kind, - }).Errorf("Failed to publish event: %v", err) + "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, - }).Errorf("Failed to save state to nostr event: %v", err) + }).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") + switch nip47Request.Method { - case MULTI_PAY_INVOICE_METHOD: - svc.HandleMultiPayInvoiceEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) - case MULTI_PAY_KEYSEND_METHOD: - svc.HandleMultiPayKeysendEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) - case PAY_INVOICE_METHOD: - svc.HandlePayInvoiceEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) - case PAY_KEYSEND_METHOD: - svc.HandlePayKeysendEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) - case GET_BALANCE_METHOD: - svc.HandleGetBalanceEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) - case MAKE_INVOICE_METHOD: - svc.HandleMakeInvoiceEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) - case LOOKUP_INVOICE_METHOD: - svc.HandleLookupInvoiceEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) - case LIST_TRANSACTIONS_METHOD: - svc.HandleListTransactionsEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) - case GET_INFO_METHOD: - svc.HandleGetInfoEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) - case SIGN_MESSAGE_METHOD: - svc.HandleSignMessageEvent(ctx, nip47Request, &requestEvent, &app, publishResponse) + 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: - svc.HandleUnknownMethod(ctx, nip47Request, publishResponse) + 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) HandleUnknownMethod(ctx context.Context, nip47Request *Request, publishResponse func(*Response, nostr.Tags)) { - publishResponse(&Response{ - ResultType: nip47Request.Method, - Error: &Error{ - Code: 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 { @@ -281,7 +347,7 @@ func (svc *nip47Service) CreateResponse(initialEvent *nostr.Event, content inter resp := &nostr.Event{ PubKey: svc.cfg.GetNostrPublicKey(), CreatedAt: nostr.Now(), - Kind: RESPONSE_KIND, + Kind: models.RESPONSE_KIND, Tags: allTags, Content: msg, } @@ -292,41 +358,7 @@ func (svc *nip47Service) CreateResponse(initialEvent *nostr.Event, content inter return resp, nil } -func (svc *nip47Service) 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, PAY_INVOICE_METHOD) { - // all payment methods are tied to the pay_invoice permission - requestMethods = append(requestMethods, PAY_KEYSEND_METHOD, MULTI_PAY_INVOICE_METHOD, MULTI_PAY_KEYSEND_METHOD) - } - - return requestMethods -} - -func (svc *nip47Service) decodeNip47Request(nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, methodParams interface{}) *Response { - err := json.Unmarshal(nip47Request.Params, methodParams) - if err != nil { - logger.Logger.WithFields(logrus.Fields{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - }).Errorf("Failed to decode nostr event: %v", err) - return &Response{ - ResultType: nip47Request.Method, - Error: &Error{ - Code: ERROR_BAD_REQUEST, - Message: err.Error(), - }} - } - return nil -} - -func (svc *nip47Service) PublishResponseEvent(ctx context.Context, sub *nostr.Subscription, requestEvent *db.RequestEvent, resp *nostr.Event, app *db.App) error { +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 @@ -338,7 +370,7 @@ func (svc *nip47Service) PublishResponseEvent(ctx context.Context, sub *nostr.Su "requestEventNostrId": requestEvent.NostrId, "appId": appId, "replyEventId": resp.ID, - }).Errorf("Failed to save response/reply event: %v", err) + }).WithError(err).Error("Failed to save response/reply event") return err } @@ -351,7 +383,7 @@ func (svc *nip47Service) PublishResponseEvent(ctx context.Context, sub *nostr.Su "appId": appId, "responseEventId": responseEvent.ID, "responseNostrEventId": resp.ID, - }).Errorf("Failed to publish reply: %v", err) + }).WithError(err).Error("Failed to publish reply") } else { responseEvent.State = db.RESPONSE_EVENT_STATE_PUBLISH_CONFIRMED responseEvent.RepliedAt = time.Now() @@ -372,7 +404,7 @@ func (svc *nip47Service) PublishResponseEvent(ctx context.Context, sub *nostr.Su "appId": appId, "responseEventId": responseEvent.ID, "responseNostrEventId": resp.ID, - }).Errorf("Failed to update response/reply event: %v", err) + }).WithError(err).Error("Failed to update response/reply event") return err } diff --git a/tests/nip47_event_handler_test.go b/nip47/event_handler_test.go similarity index 51% rename from tests/nip47_event_handler_test.go rename to nip47/event_handler_test.go index da723cf77..d5ba35f89 100644 --- a/tests/nip47_event_handler_test.go +++ b/nip47/event_handler_test.go @@ -1,18 +1,23 @@ -package tests +package nip47 import ( "encoding/json" "testing" - "github.com/getAlby/nostr-wallet-connect/nip47" + "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 removeTestService() - svc, err := createTestService() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() assert.NoError(t, err) reqPrivateKey := nostr.GeneratePrivateKey() @@ -20,36 +25,43 @@ func TestCreateResponse(t *testing.T) { assert.NoError(t, err) reqEvent := &nostr.Event{ - Kind: nip47.REQUEST_KIND, + Kind: models.REQUEST_KIND, PubKey: reqPubkey, Content: "1", } reqEvent.ID = "12345" - ss, err := nip04.ComputeSharedSecret(reqPubkey, svc.cfg.GetNostrSecretKey()) + 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, + type dummyResponse struct { + Foo int + } + + nip47Response := &models.Response{ + ResultType: "dummy_method", + Result: dummyResponse{ + Foo: 1000, }, } - res, err := svc.nip47Svc.CreateResponse(reqEvent, nip47Response, nostr.Tags{}, ss) + + nip47svc := NewNip47Service(svc.DB, svc.Cfg, 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.cfg.GetNostrPublicKey(), res.PubKey) + 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{}, + 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.(*nip47.BalanceResponse)) + assert.Equal(t, nip47Response.Result, *unmarshalledResponse.Result.(*dummyResponse)) } diff --git a/nip47/handle_balance_request.go b/nip47/handle_balance_request.go deleted file mode 100644 index 9b2ee634f..000000000 --- a/nip47/handle_balance_request.go +++ /dev/null @@ -1,62 +0,0 @@ -package nip47 - -import ( - "context" - - "github.com/getAlby/nostr-wallet-connect/db" - "github.com/getAlby/nostr-wallet-connect/logger" - "github.com/nbd-wtf/go-nostr" - "github.com/sirupsen/logrus" -) - -const ( - MSAT_PER_SAT = 1000 -) - -func (svc *nip47Service) HandleGetBalanceEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) { - - resp := svc.checkPermission(nip47Request, requestEvent.NostrId, app, 0) - if resp != nil { - publishResponse(resp, nostr.Tags{}) - return - } - - logger.Logger.WithFields(logrus.Fields{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - }).Info("Fetching balance") - - balance, err := svc.lnClient.GetBalance(ctx) - if err != nil { - logger.Logger.WithFields(logrus.Fields{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - }).Infof("Failed to fetch balance: %v", err) - publishResponse(&Response{ - ResultType: nip47Request.Method, - Error: &Error{ - Code: ERROR_INTERNAL, - Message: err.Error(), - }, - }, nostr.Tags{}) - return - } - - responsePayload := &BalanceResponse{ - Balance: balance, - } - - appPermission := db.AppPermission{} - svc.db.Where("app_id = ? AND request_method = ?", app.ID, PAY_INVOICE_METHOD).First(&appPermission) - - maxAmount := appPermission.MaxAmount - if maxAmount > 0 { - responsePayload.MaxAmount = maxAmount * MSAT_PER_SAT - responsePayload.BudgetRenewal = appPermission.BudgetRenewal - } - - publishResponse(&Response{ - ResultType: nip47Request.Method, - Result: responsePayload, - }, nostr.Tags{}) -} diff --git a/nip47/handle_info_request.go b/nip47/handle_info_request.go deleted file mode 100644 index fc4f79f45..000000000 --- a/nip47/handle_info_request.go +++ /dev/null @@ -1,62 +0,0 @@ -package nip47 - -import ( - "context" - - "github.com/getAlby/nostr-wallet-connect/db" - "github.com/getAlby/nostr-wallet-connect/logger" - "github.com/nbd-wtf/go-nostr" - "github.com/sirupsen/logrus" -) - -func (svc *nip47Service) HandleGetInfoEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) { - - resp := svc.checkPermission(nip47Request, requestEvent.NostrId, app, 0) - if resp != nil { - publishResponse(resp, nostr.Tags{}) - return - } - - logger.Logger.WithFields(logrus.Fields{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - }).Info("Fetching node info") - - info, err := svc.lnClient.GetInfo(ctx) - if err != nil { - logger.Logger.WithFields(logrus.Fields{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - }).Infof("Failed to fetch node info: %v", err) - - publishResponse(&Response{ - ResultType: nip47Request.Method, - Error: &Error{ - Code: 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: svc.GetMethods(app), - } - - publishResponse(&Response{ - ResultType: nip47Request.Method, - Result: responsePayload, - }, nostr.Tags{}) -} diff --git a/nip47/handle_list_transactions_request.go b/nip47/handle_list_transactions_request.go deleted file mode 100644 index 85ab36e85..000000000 --- a/nip47/handle_list_transactions_request.go +++ /dev/null @@ -1,65 +0,0 @@ -package nip47 - -import ( - "context" - - "github.com/getAlby/nostr-wallet-connect/db" - "github.com/getAlby/nostr-wallet-connect/logger" - "github.com/nbd-wtf/go-nostr" - "github.com/sirupsen/logrus" -) - -func (svc *nip47Service) HandleListTransactionsEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) { - - listParams := &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 - } - - logger.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 { - logger.Logger.WithFields(logrus.Fields{ - // TODO: log request fields from listParams - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - }).Infof("Failed to fetch transactions: %v", err) - - publishResponse(&Response{ - ResultType: nip47Request.Method, - Error: &Error{ - Code: ERROR_INTERNAL, - Message: err.Error(), - }, - }, nostr.Tags{}) - return - } - - responsePayload := &ListTransactionsResponse{ - Transactions: transactions, - } - - publishResponse(&Response{ - ResultType: nip47Request.Method, - Result: responsePayload, - }, nostr.Tags{}) -} diff --git a/nip47/handle_lookup_invoice_request.go b/nip47/handle_lookup_invoice_request.go deleted file mode 100644 index dafb3b813..000000000 --- a/nip47/handle_lookup_invoice_request.go +++ /dev/null @@ -1,87 +0,0 @@ -package nip47 - -import ( - "context" - "fmt" - "strings" - - "github.com/getAlby/nostr-wallet-connect/db" - "github.com/getAlby/nostr-wallet-connect/logger" - "github.com/nbd-wtf/go-nostr" - decodepay "github.com/nbd-wtf/ln-decodepay" - "github.com/sirupsen/logrus" -) - -func (svc *nip47Service) HandleLookupInvoiceEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) { - - lookupInvoiceParams := &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 - } - - logger.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 { - logger.Logger.WithFields(logrus.Fields{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - "invoice": lookupInvoiceParams.Invoice, - }).Errorf("Failed to decode bolt11 invoice: %v", err) - - publishResponse(&Response{ - ResultType: nip47Request.Method, - Error: &Error{ - Code: 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 { - logger.Logger.WithFields(logrus.Fields{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - "invoice": lookupInvoiceParams.Invoice, - "paymentHash": lookupInvoiceParams.PaymentHash, - }).Infof("Failed to lookup invoice: %v", err) - - publishResponse(&Response{ - ResultType: nip47Request.Method, - Error: &Error{ - Code: ERROR_INTERNAL, - Message: err.Error(), - }, - }, nostr.Tags{}) - return - } - - responsePayload := &LookupInvoiceResponse{ - Transaction: *transaction, - } - - publishResponse(&Response{ - ResultType: nip47Request.Method, - Result: responsePayload, - }, nostr.Tags{}) -} diff --git a/nip47/handle_make_invoice_request.go b/nip47/handle_make_invoice_request.go deleted file mode 100644 index 0a95606fe..000000000 --- a/nip47/handle_make_invoice_request.go +++ /dev/null @@ -1,70 +0,0 @@ -package nip47 - -import ( - "context" - - "github.com/getAlby/nostr-wallet-connect/db" - "github.com/getAlby/nostr-wallet-connect/logger" - "github.com/nbd-wtf/go-nostr" - "github.com/sirupsen/logrus" -) - -func (svc *nip47Service) HandleMakeInvoiceEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) { - - makeInvoiceParams := &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 - } - - logger.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 { - logger.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(&Response{ - ResultType: nip47Request.Method, - Error: &Error{ - Code: ERROR_INTERNAL, - Message: err.Error(), - }, - }, nostr.Tags{}) - return - } - - responsePayload := &MakeInvoiceResponse{ - Transaction: *transaction, - } - - publishResponse(&Response{ - ResultType: nip47Request.Method, - Result: responsePayload, - }, nostr.Tags{}) -} diff --git a/nip47/handle_multi_pay_invoice_request.go b/nip47/handle_multi_pay_invoice_request.go deleted file mode 100644 index 025b881ff..000000000 --- a/nip47/handle_multi_pay_invoice_request.go +++ /dev/null @@ -1,139 +0,0 @@ -package nip47 - -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/logger" - "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 *nip47Service) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) { - - multiPayParams := &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 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{ - "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(&Response{ - ResultType: nip47Request.Method, - Error: &Error{ - Code: 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 { - logger.Logger.WithFields(logrus.Fields{ - "requestEventNostrId": requestEvent.NostrId, - "paymentRequest": bolt11, - "invoiceId": invoiceInfo.Id, - }).Errorf("Failed to process event: %v", insertPaymentResult.Error) - return - } - - logger.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 { - logger.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": err.Error(), - "multi": true, - "invoice": bolt11, - "amount": paymentRequest.MSatoshi / 1000, - }, - }) - - publishResponse(&Response{ - ResultType: nip47Request.Method, - Error: &Error{ - Code: 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(&Response{ - ResultType: nip47Request.Method, - Result: PayResponse{ - Preimage: response.Preimage, - FeesPaid: response.Fee, - }, - }, nostr.Tags{dTag}) - }(invoiceInfo) - } - - wg.Wait() -} diff --git a/nip47/handle_multi_pay_keysend_request.go b/nip47/handle_multi_pay_keysend_request.go deleted file mode 100644 index ad4ef3892..000000000 --- a/nip47/handle_multi_pay_keysend_request.go +++ /dev/null @@ -1,109 +0,0 @@ -package nip47 - -import ( - "context" - "sync" - - "github.com/getAlby/nostr-wallet-connect/db" - "github.com/getAlby/nostr-wallet-connect/events" - "github.com/getAlby/nostr-wallet-connect/logger" - "github.com/nbd-wtf/go-nostr" - "github.com/sirupsen/logrus" -) - -func (svc *nip47Service) HandleMultiPayKeysendEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) { - - multiPayParams := &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 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 { - logger.Logger.WithFields(logrus.Fields{ - "requestEventNostrId": requestEvent.NostrId, - "recipientPubkey": keysendInfo.Pubkey, - "keysendId": keysendInfo.Id, - }).Errorf("Failed to process event: %v", insertPaymentResult.Error) - return - } - - logger.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 { - logger.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": err.Error(), - "keysend": true, - "multi": true, - "amount": keysendInfo.Amount / 1000, - }, - }) - - publishResponse(&Response{ - ResultType: nip47Request.Method, - Error: &Error{ - Code: 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(&Response{ - ResultType: nip47Request.Method, - Result: PayResponse{ - Preimage: preimage, - }, - }, nostr.Tags{dTag}) - }(keysendInfo) - } - - wg.Wait() -} diff --git a/nip47/handle_pay_keysend_request.go b/nip47/handle_pay_keysend_request.go deleted file mode 100644 index 86989bbca..000000000 --- a/nip47/handle_pay_keysend_request.go +++ /dev/null @@ -1,86 +0,0 @@ -package nip47 - -import ( - "context" - - "github.com/getAlby/nostr-wallet-connect/db" - "github.com/getAlby/nostr-wallet-connect/events" - "github.com/getAlby/nostr-wallet-connect/logger" - "github.com/nbd-wtf/go-nostr" - "github.com/sirupsen/logrus" -) - -func (svc *nip47Service) HandlePayKeysendEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) { - - payParams := &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(&Response{ - ResultType: nip47Request.Method, - Error: &Error{ - Code: ERROR_INTERNAL, - Message: err.Error(), - }, - }, nostr.Tags{}) - return - } - - logger.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 { - logger.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": err.Error(), - "keysend": true, - "amount": payParams.Amount / 1000, - }, - }) - publishResponse(&Response{ - ResultType: nip47Request.Method, - Error: &Error{ - Code: 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(&Response{ - ResultType: nip47Request.Method, - Result: PayResponse{ - Preimage: preimage, - }, - }, nostr.Tags{}) -} diff --git a/nip47/handle_payment_request.go b/nip47/handle_payment_request.go deleted file mode 100644 index 84b96666b..000000000 --- a/nip47/handle_payment_request.go +++ /dev/null @@ -1,114 +0,0 @@ -package nip47 - -import ( - "context" - "fmt" - "strings" - - "github.com/getAlby/nostr-wallet-connect/db" - "github.com/getAlby/nostr-wallet-connect/events" - "github.com/getAlby/nostr-wallet-connect/logger" - "github.com/nbd-wtf/go-nostr" - decodepay "github.com/nbd-wtf/ln-decodepay" - "github.com/sirupsen/logrus" -) - -func (svc *nip47Service) HandlePayInvoiceEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) { - - payParams := &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 { - logger.Logger.WithFields(logrus.Fields{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - "bolt11": bolt11, - }).Errorf("Failed to decode bolt11 invoice: %v", err) - - publishResponse(&Response{ - ResultType: nip47Request.Method, - Error: &Error{ - Code: 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(&Response{ - ResultType: nip47Request.Method, - Error: &Error{ - Code: ERROR_INTERNAL, - Message: err.Error(), - }, - }, nostr.Tags{}) - return - } - - logger.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 { - logger.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": err.Error(), - "invoice": bolt11, - "amount": paymentRequest.MSatoshi / 1000, - }, - }) - publishResponse(&Response{ - ResultType: nip47Request.Method, - Error: &Error{ - Code: 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(&Response{ - ResultType: nip47Request.Method, - Result: PayResponse{ - Preimage: response.Preimage, - FeesPaid: response.Fee, - }, - }, nostr.Tags{}) -} diff --git a/nip47/handle_sign_message_request.go b/nip47/handle_sign_message_request.go deleted file mode 100644 index d5a7f80ce..000000000 --- a/nip47/handle_sign_message_request.go +++ /dev/null @@ -1,56 +0,0 @@ -package nip47 - -import ( - "context" - - "github.com/getAlby/nostr-wallet-connect/db" - "github.com/getAlby/nostr-wallet-connect/logger" - "github.com/nbd-wtf/go-nostr" - "github.com/sirupsen/logrus" -) - -func (svc *nip47Service) HandleSignMessageEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) { - signParams := &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 - } - - logger.Logger.WithFields(logrus.Fields{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - }).Info("Signing message") - - signature, err := svc.lnClient.SignMessage(ctx, signParams.Message) - if err != nil { - logger.Logger.WithFields(logrus.Fields{ - "requestEventNostrId": requestEvent.NostrId, - "appId": app.ID, - }).Infof("Failed to sign message: %v", err) - publishResponse(&Response{ - ResultType: nip47Request.Method, - Error: &Error{ - Code: ERROR_INTERNAL, - Message: err.Error(), - }, - }, nostr.Tags{}) - return - } - - responsePayload := SignMessageResponse{ - Message: signParams.Message, - Signature: signature, - } - - publishResponse(&Response{ - ResultType: nip47Request.Method, - Result: responsePayload, - }, nostr.Tags{}) -} diff --git a/nip47/models.go b/nip47/models.go deleted file mode 100644 index d97da364a..000000000 --- a/nip47/models.go +++ /dev/null @@ -1,198 +0,0 @@ -package nip47 - -import ( - "context" - "encoding/json" - - "github.com/getAlby/nostr-wallet-connect/db" - "github.com/getAlby/nostr-wallet-connect/lnclient" - "github.com/nbd-wtf/go-nostr" -) - -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"` -} - -type Nip47Service interface { - HasPermission(app *db.App, requestMethod string, amount int64) (result bool, code string, message string) - GetBudgetUsage(appPermission *db.AppPermission) int64 - StartNotifier(ctx context.Context, relay *nostr.Relay, lnClient lnclient.LNClient) - HandleEvent(ctx context.Context, sub *nostr.Subscription, event *nostr.Event) - PublishNip47Info(ctx context.Context, relay *nostr.Relay) error - Stop() // TODO: remove and replace with context - - CreateResponse(initialEvent *nostr.Event, content interface{}, tags nostr.Tags, ss []byte) (result *nostr.Event, err error) - HandleUnknownMethod(ctx context.Context, nip47Request *Request, publishResponse func(*Response, nostr.Tags)) - HandleGetBalanceEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) - HandleGetInfoEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) - HandleListTransactionsEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) - HandleLookupInvoiceEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) - HandleMakeInvoiceEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) - HandleMultiPayInvoiceEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) - HandleMultiPayKeysendEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) - HandlePayKeysendEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) - HandlePayInvoiceEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) - HandleSignMessageEvent(ctx context.Context, nip47Request *Request, requestEvent *db.RequestEvent, app *db.App, publishResponse func(*Response, nostr.Tags)) -} 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 index ff46b23d5..ba1974303 100644 --- a/nip47/nip47_service.go +++ b/nip47/nip47_service.go @@ -6,36 +6,41 @@ 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/nip47/notifications" + permissions "github.com/getAlby/nostr-wallet-connect/nip47/permissions" "github.com/nbd-wtf/go-nostr" "gorm.io/gorm" ) type nip47Service struct { + permissionsService permissions.PermissionsService + nip47NotificationQueue notifications.Nip47NotificationQueue + cfg config.Config db *gorm.DB - nip47NotificationQueue Nip47NotificationQueue eventPublisher events.EventPublisher - cfg config.Config - lnClient lnclient.LNClient } -func NewNip47Service(db *gorm.DB, eventPublisher events.EventPublisher, cfg config.Config, lnClient lnclient.LNClient) *nip47Service { - nip47NotificationQueue := NewNip47NotificationQueue() +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, 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, - cfg: cfg, - lnClient: lnClient, } } -func (svc *nip47Service) Stop() { - svc.eventPublisher.RemoveSubscriber(svc.nip47NotificationQueue) -} - func (svc *nip47Service) StartNotifier(ctx context.Context, relay *nostr.Relay, lnClient lnclient.LNClient) { - nip47Notifier := NewNip47Notifier(svc, relay, svc.cfg, svc.db, lnClient) + nip47Notifier := notifications.NewNip47Notifier(relay, svc.db, svc.cfg, svc.permissionsService, lnClient) go func() { for { select { 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 97% rename from nip47/nip47_notification_queue.go rename to nip47/notifications/nip47_notification_queue.go index 5ef96d474..59bf954b2 100644 --- a/nip47/nip47_notification_queue.go +++ b/nip47/notifications/nip47_notification_queue.go @@ -1,4 +1,4 @@ -package nip47 +package notifications import ( "context" diff --git a/nip47/nip47_notifier.go b/nip47/notifications/nip47_notifier.go similarity index 83% rename from nip47/nip47_notifier.go rename to nip47/notifications/nip47_notifier.go index 2749221db..2c25de708 100644 --- a/nip47/nip47_notifier.go +++ b/nip47/notifications/nip47_notifier.go @@ -1,4 +1,4 @@ -package nip47 +package notifications import ( "context" @@ -10,6 +10,8 @@ import ( "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/getAlby/nostr-wallet-connect/nip47/permissions" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip04" "github.com/sirupsen/logrus" @@ -21,20 +23,20 @@ type Relay interface { } type Nip47Notifier struct { - db *gorm.DB - nip47Svc *nip47Service - relay Relay - cfg config.Config - lnClient lnclient.LNClient + relay Relay + cfg config.Config + lnClient lnclient.LNClient + db *gorm.DB + permissionsSvc permissions.PermissionsService } -func NewNip47Notifier(nip47Svc *nip47Service, relay Relay, cfg config.Config, db *gorm.DB, lnClient lnclient.LNClient) *Nip47Notifier { +func NewNip47Notifier(relay Relay, db *gorm.DB, cfg config.Config, permissionsSvc permissions.PermissionsService, lnClient lnclient.LNClient) *Nip47Notifier { return &Nip47Notifier{ - nip47Svc: nip47Svc, - relay: relay, - cfg: cfg, - db: db, - lnClient: lnClient, + relay: relay, + cfg: cfg, + db: db, + lnClient: lnClient, + permissionsSvc: permissionsSvc, } } @@ -72,7 +74,7 @@ func (notifier *Nip47Notifier) notifySubscribers(ctx context.Context, notificati notifier.db.Find(&apps) for _, app := range apps { - hasPermission, _, _ := notifier.nip47Svc.HasPermission(&app, NOTIFICATIONS_PERMISSION, 0) + hasPermission, _, _ := notifier.permissionsSvc.HasPermission(&app, permissions.NOTIFICATIONS_PERMISSION, 0) if !hasPermission { continue } @@ -118,7 +120,7 @@ func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App event := &nostr.Event{ PubKey: notifier.cfg.GetNostrPublicKey(), CreatedAt: nostr.Now(), - Kind: NOTIFICATION_KIND, + Kind: models.NOTIFICATION_KIND, Tags: allTags, Content: msg, } diff --git a/nip47/notifications/nip47_notifier_test.go b/nip47/notifications/nip47_notifier_test.go new file mode 100644 index 000000000..9d3f218a2 --- /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, 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, 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.go b/nip47/permissions.go deleted file mode 100644 index 6d3a693e6..000000000 --- a/nip47/permissions.go +++ /dev/null @@ -1,83 +0,0 @@ -package nip47 - -import ( - "fmt" - "time" - - "github.com/getAlby/nostr-wallet-connect/db" - "github.com/getAlby/nostr-wallet-connect/events" - "github.com/getAlby/nostr-wallet-connect/logger" - "github.com/sirupsen/logrus" -) - -func (svc *nip47Service) HasPermission(app *db.App, requestMethod string, amount int64) (result bool, code string, message string) { - switch requestMethod { - case PAY_INVOICE_METHOD, PAY_KEYSEND_METHOD, MULTI_PAY_INVOICE_METHOD, MULTI_PAY_KEYSEND_METHOD: - requestMethod = 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, 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, ERROR_EXPIRED, "This app has expired" - } - - if requestMethod == PAY_INVOICE_METHOD { - maxAmount := appPermission.MaxAmount - if maxAmount != 0 { - budgetUsage := svc.GetBudgetUsage(&appPermission) - - if budgetUsage+amount/1000 > int64(maxAmount) { - return false, ERROR_QUOTA_EXCEEDED, "Insufficient budget remaining to make payment" - } - } - } - return true, "", "" -} - -func (svc *nip47Service) checkPermission(nip47Request *Request, requestNostrEventId string, app *db.App, amount int64) *Response { - hasPermission, code, message := svc.HasPermission(app, nip47Request.Method, amount) - if !hasPermission { - logger.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 &Response{ - ResultType: nip47Request.Method, - Error: &Error{ - Code: code, - Message: message, - }, - } - } - 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/tests/nip47_permissions_test.go b/nip47/permissions/permissions_test.go similarity index 50% rename from tests/nip47_permissions_test.go rename to nip47/permissions/permissions_test.go index 43532445f..9e7f19fb5 100644 --- a/tests/nip47_permissions_test.go +++ b/nip47/permissions/permissions_test.go @@ -1,34 +1,36 @@ -package tests +package permissions import ( "testing" "time" "github.com/getAlby/nostr-wallet-connect/db" - "github.com/getAlby/nostr-wallet-connect/nip47" + "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 removeTestService() - svc, err := createTestService() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() assert.NoError(t, err) - app, _, err := createApp(svc) + app, _, err := tests.CreateApp(svc) assert.NoError(t, err) - result, code, message := svc.nip47Svc.HasPermission(app, nip47.PAY_INVOICE_METHOD, 100) + permissionsSvc := NewPermissionsService(svc.DB, svc.EventPublisher) + result, code, message := permissionsSvc.HasPermission(app, models.PAY_INVOICE_METHOD, 100) assert.False(t, result) - assert.Equal(t, nip47.ERROR_RESTRICTED, code) + 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 removeTestService() - svc, err := createTestService() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() assert.NoError(t, err) - app, _, err := createApp(svc) + app, _, err := tests.CreateApp(svc) assert.NoError(t, err) budgetRenewal := "never" @@ -36,26 +38,27 @@ func TestHasPermission_Expired(t *testing.T) { appPermission := &db.AppPermission{ AppId: app.ID, App: *app, - RequestMethod: nip47.PAY_INVOICE_METHOD, + RequestMethod: models.PAY_INVOICE_METHOD, MaxAmount: 100, BudgetRenewal: budgetRenewal, ExpiresAt: &expiresAt, } - err = svc.db.Create(appPermission).Error + err = svc.DB.Create(appPermission).Error assert.NoError(t, err) - result, code, message := svc.nip47Svc.HasPermission(app, nip47.PAY_INVOICE_METHOD, 100) + permissionsSvc := NewPermissionsService(svc.DB, svc.EventPublisher) + result, code, message := permissionsSvc.HasPermission(app, models.PAY_INVOICE_METHOD, 100) assert.False(t, result) - assert.Equal(t, nip47.ERROR_EXPIRED, code) + assert.Equal(t, models.ERROR_EXPIRED, code) assert.Equal(t, "This app has expired", message) } func TestHasPermission_Exceeded(t *testing.T) { - defer removeTestService() - svc, err := createTestService() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() assert.NoError(t, err) - app, _, err := createApp(svc) + app, _, err := tests.CreateApp(svc) assert.NoError(t, err) budgetRenewal := "never" @@ -63,26 +66,27 @@ func TestHasPermission_Exceeded(t *testing.T) { appPermission := &db.AppPermission{ AppId: app.ID, App: *app, - RequestMethod: nip47.PAY_INVOICE_METHOD, + RequestMethod: models.PAY_INVOICE_METHOD, MaxAmount: 10, BudgetRenewal: budgetRenewal, ExpiresAt: &expiresAt, } - err = svc.db.Create(appPermission).Error + err = svc.DB.Create(appPermission).Error assert.NoError(t, err) - result, code, message := svc.nip47Svc.HasPermission(app, nip47.PAY_INVOICE_METHOD, 100*1000) + 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, nip47.ERROR_QUOTA_EXCEEDED, code) + 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 removeTestService() - svc, err := createTestService() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() assert.NoError(t, err) - app, _, err := createApp(svc) + app, _, err := tests.CreateApp(svc) assert.NoError(t, err) budgetRenewal := "never" @@ -90,15 +94,16 @@ func TestHasPermission_OK(t *testing.T) { appPermission := &db.AppPermission{ AppId: app.ID, App: *app, - RequestMethod: nip47.PAY_INVOICE_METHOD, + RequestMethod: models.PAY_INVOICE_METHOD, MaxAmount: 10, BudgetRenewal: budgetRenewal, ExpiresAt: &expiresAt, } - err = svc.db.Create(appPermission).Error + err = svc.DB.Create(appPermission).Error assert.NoError(t, err) - result, code, message := svc.nip47Svc.HasPermission(app, nip47.PAY_INVOICE_METHOD, 10*1000) + 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 index dc18e1f55..d912cb263 100644 --- a/nip47/publish_nip47_info.go +++ b/nip47/publish_nip47_info.go @@ -4,16 +4,18 @@ 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 = INFO_EVENT_KIND - ev.Content = CAPABILITIES + ev.Kind = models.INFO_EVENT_KIND + ev.Content = models.CAPABILITIES ev.CreatedAt = nostr.Now() ev.PubKey = svc.cfg.GetNostrPublicKey() - ev.Tags = nostr.Tags{[]string{"notifications", NOTIFICATION_TYPES}} + ev.Tags = nostr.Tags{[]string{"notifications", notifications.NOTIFICATION_TYPES}} err := ev.Sign(svc.cfg.GetNostrSecretKey()) if err != nil { return err diff --git a/service/models.go b/service/models.go index 6034e5cea..58f6eeb04 100644 --- a/service/models.go +++ b/service/models.go @@ -5,20 +5,19 @@ 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/nip47" "gorm.io/gorm" ) type Service interface { - GetLNClient() lnclient.LNClient - GetConfig() config.Config StartApp(encryptionKey string) error StopApp() StopLNClient() error - StopDb() error - GetAlbyOAuthSvc() alby.AlbyOAuthService - GetNip47Service() nip47.Nip47Service - GetDB() *gorm.DB 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 } diff --git a/service/service.go b/service/service.go index a5e771159..beddbfc75 100644 --- a/service/service.go +++ b/service/service.go @@ -2,7 +2,7 @@ package service import ( "context" - "database/sql" + "errors" "fmt" "net/http" @@ -16,13 +16,12 @@ import ( "github.com/adrg/xdg" "github.com/nbd-wtf/go-nostr" "gopkg.in/DataDog/dd-trace-go.v1/profiler" + "gorm.io/gorm" - "github.com/glebarez/sqlite" "github.com/joho/godotenv" "github.com/kelseyhightower/envconfig" - "gorm.io/gorm" - alby "github.com/getAlby/nostr-wallet-connect/alby" + "github.com/getAlby/nostr-wallet-connect/alby" "github.com/getAlby/nostr-wallet-connect/events" "github.com/getAlby/nostr-wallet-connect/logger" @@ -35,12 +34,13 @@ import ( "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" + "github.com/getAlby/nostr-wallet-connect/nip47/models" ) type service struct { - cfg config.Config + cfg config.Config + db *gorm.DB lnClient lnclient.LNClient albyOAuthSvc alby.AlbyOAuthService @@ -85,33 +85,12 @@ func NewService(ctx context.Context) (*service, error) { } } - 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() + gormDB, err := db.NewDB(appConfig.DatabaseUri) if err != nil { return nil, err } - sqlDb.SetMaxOpenConns(1) - err = migrations.Migrate(gormDB, appConfig) - if err != nil { - logger.Logger.WithError(err).Error("Failed to migrate") - return nil, err - } - - cfg := config.NewConfig(gormDB, appConfig) + cfg := config.NewConfig(appConfig, gormDB) eventPublisher := events.NewEventPublisher() @@ -123,11 +102,12 @@ func NewService(ctx context.Context) (*service, error) { var wg sync.WaitGroup svc := &service{ cfg: cfg, - db: gormDB, ctx: ctx, wg: &wg, eventPublisher: eventPublisher, - albyOAuthSvc: alby.NewAlbyOAuthService(cfg, cfg.GetEnv(), db.NewDBService(gormDB)), + albyOAuthSvc: alby.NewAlbyOAuthService(gormDB, cfg), + nip47Service: nip47.NewNip47Service(gormDB, cfg, eventPublisher), + db: gormDB, } eventPublisher.RegisterSubscriber(svc.albyOAuthSvc) @@ -166,8 +146,6 @@ func (svc *service) StopLNClient() error { svc.eventPublisher.Publish(&events.Event{ Event: "nwc_node_stopped", }) - // TODO: move this, use contexts instead - svc.nip47Service.Stop() } logger.Logger.Info("LNClient stopped successfully") return nil @@ -249,7 +227,7 @@ func (svc *service) launchLNBackend(ctx context.Context, encryptionKey string) e func (svc *service) createFilters(identityPubkey string) nostr.Filters { filter := nostr.Filter{ Tags: nostr.TagMap{"p": []string{identityPubkey}}, - Kinds: []int{nip47.REQUEST_KIND}, + Kinds: []int{models.REQUEST_KIND}, } return []nostr.Filter{filter} } @@ -272,7 +250,7 @@ func (svc *service) StartSubscription(ctx context.Context, sub *nostr.Subscripti // loop through incoming events for event := range sub.Events { - go svc.nip47Service.HandleEvent(ctx, sub, event) + go svc.nip47Service.HandleEvent(ctx, sub, event, svc.lnClient) } logger.Logger.Info("Relay subscription events channel ended") }() @@ -378,17 +356,8 @@ func startDataDogProfiler(ctx context.Context) { }() } -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) GetDB() *gorm.DB { + return svc.db } func (svc *service) GetConfig() config.Config { @@ -403,10 +372,6 @@ func (svc *service) GetNip47Service() nip47.Nip47Service { return svc.nip47Service } -func (svc *service) GetDB() *gorm.DB { - return svc.db -} - func (svc *service) GetEventPublisher() events.EventPublisher { return svc.eventPublisher } diff --git a/service/start.go b/service/start.go index c42389d9d..cb5fc6290 100644 --- a/service/start.go +++ b/service/start.go @@ -11,13 +11,10 @@ import ( "github.com/getAlby/nostr-wallet-connect/events" "github.com/getAlby/nostr-wallet-connect/logger" - "github.com/getAlby/nostr-wallet-connect/nip47" ) func (svc *service) StartNostr(ctx context.Context, encryptionKey string) error { - svc.nip47Service = nip47.NewNip47Service(svc.db, svc.eventPublisher, svc.cfg, svc.lnClient) - relayUrl := svc.cfg.GetRelayUrl() err := svc.cfg.Start(encryptionKey) diff --git a/tests/create_app.go b/tests/create_app.go index f904c5901..0d20acbbf 100644 --- a/tests/create_app.go +++ b/tests/create_app.go @@ -6,20 +6,20 @@ import ( "github.com/nbd-wtf/go-nostr/nip04" ) -func createApp(svc *TestService) (app *db.App, ss []byte, err error) { +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.cfg.GetNostrPublicKey(), senderPrivkey) + 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 + err = svc.DB.Create(app).Error if err != nil { return nil, nil, err } diff --git a/tests/create_test_service.go b/tests/create_test_service.go deleted file mode 100644 index 12cead554..000000000 --- a/tests/create_test_service.go +++ /dev/null @@ -1,72 +0,0 @@ -package tests - -import ( - "os" - - "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/migrations" - "github.com/getAlby/nostr-wallet-connect/nip47" - "github.com/glebarez/sqlite" - "github.com/sirupsen/logrus" - "gorm.io/gorm" -) - -const testDB = "test.db" - -func createTestService() (svc *TestService, err error) { - gormDb, err := gorm.Open(sqlite.Open(testDB), &gorm.Config{}) - if err != nil { - return nil, err - } - - mockLn, err := NewMockLn() - if err != nil { - return nil, err - } - - logger.Logger = logrus.New() - logger.Logger.SetFormatter(&logrus.JSONFormatter{}) - logger.Logger.SetOutput(os.Stdout) - logger.Logger.SetLevel(logrus.InfoLevel) - - appConfig := &config.AppConfig{ - Workdir: ".test", - } - - err = migrations.Migrate(gormDb, appConfig) - if err != nil { - return nil, err - } - - cfg := config.NewConfig( - gormDb, - appConfig, - ) - - cfg.Start("") - - eventPublisher := events.NewEventPublisher() - - return &TestService{ - cfg: cfg, - db: gormDb, - lnClient: mockLn, - eventPublisher: eventPublisher, - nip47Svc: nip47.NewNip47Service(gormDb, eventPublisher, cfg, mockLn), - }, nil -} - -type TestService struct { - cfg config.Config - db *gorm.DB - lnClient lnclient.LNClient - eventPublisher events.EventPublisher - nip47Svc nip47.Nip47Service -} - -func removeTestService() { - os.Remove(testDB) -} diff --git a/tests/mock_ln_client.go b/tests/mock_ln_client.go index 086bb0411..1805a416e 100644 --- a/tests/mock_ln_client.go +++ b/tests/mock_ln_client.go @@ -5,17 +5,16 @@ import ( "time" "github.com/getAlby/nostr-wallet-connect/lnclient" - "github.com/getAlby/nostr-wallet-connect/nip47" ) // for the invoice: // lnbcrt5u1pjuywzppp5h69dt59cypca2wxu69sw8ga0g39a3yx7dqug5nthrw3rcqgfdu4qdqqcqzzsxqyz5vqsp5gzlpzszyj2k30qmpme7jsfzr24wqlvt9xdmr7ay34lfelz050krs9qyyssq038x07nh8yuv8hdpjh5y8kqp7zcd62ql9na9xh7pla44htjyy02sz23q7qm2tza6ct4ypljk54w9k9qsrsu95usk8ce726ytep6vhhsq9mhf9a -const mockPaymentHash500 = "be8ad5d0b82071d538dcd160e3a3af444bd890de68388a4d771ba23c01096f2a" +const MockPaymentHash500 = "be8ad5d0b82071d538dcd160e3a3af444bd890de68388a4d771ba23c01096f2a" -const mockInvoice = "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" -const mockPaymentHash = "320c2c5a1492ccfd5bc7aa4ad9b657d6aaec3cfcc0d1d98413a29af4ac772ccf" // for the above invoice +const MockInvoice = "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" +const MockPaymentHash = "320c2c5a1492ccfd5bc7aa4ad9b657d6aaec3cfcc0d1d98413a29af4ac772ccf" // for the above invoice -var mockNodeInfo = lnclient.NodeInfo{ +var MockNodeInfo = lnclient.NodeInfo{ Alias: "bob", Color: "#3399FF", Pubkey: "123pubkey", @@ -24,20 +23,20 @@ var mockNodeInfo = lnclient.NodeInfo{ BlockHash: "123blockhash", } -var mockTime = time.Unix(1693876963, 0) -var mockTimeUnix = mockTime.Unix() +var MockTime = time.Unix(1693876963, 0) +var MockTimeUnix = MockTime.Unix() -var mockTransactions = []nip47.Transaction{ +var MockTransactions = []lnclient.Transaction{ { Type: "incoming", - Invoice: mockInvoice, + Invoice: MockInvoice, Description: "mock invoice 1", DescriptionHash: "hash1", Preimage: "preimage1", PaymentHash: "payment_hash_1", Amount: 1000, FeesPaid: 50, - SettledAt: &mockTimeUnix, + SettledAt: &MockTimeUnix, Metadata: map[string]interface{}{ "key1": "value1", "key2": 42, @@ -45,17 +44,17 @@ var mockTransactions = []nip47.Transaction{ }, { Type: "incoming", - Invoice: mockInvoice, + Invoice: MockInvoice, Description: "mock invoice 2", DescriptionHash: "hash2", Preimage: "preimage2", PaymentHash: "payment_hash_2", Amount: 2000, FeesPaid: 75, - SettledAt: &mockTimeUnix, + SettledAt: &MockTimeUnix, }, } -var mockTransaction = &mockTransactions[0] +var MockTransaction = &MockTransactions[0] type MockLn struct { } @@ -70,7 +69,7 @@ func (mln *MockLn) SendPaymentSync(ctx context.Context, payReq string) (*lnclien }, nil } -func (mln *MockLn) SendKeysend(ctx context.Context, amount int64, destination, preimage string, custom_records []lnclient.TLVRecord) (preImage string, err error) { +func (mln *MockLn) SendKeysend(ctx context.Context, amount uint64, destination, preimage string, custom_records []lnclient.TLVRecord) (preImage string, err error) { return "12345preimage", nil } @@ -79,19 +78,19 @@ func (mln *MockLn) GetBalance(ctx context.Context) (balance int64, err error) { } func (mln *MockLn) GetInfo(ctx context.Context) (info *lnclient.NodeInfo, err error) { - return &mockNodeInfo, nil + 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) 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 *nip47.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 []nip47.Transaction, err error) { - return mockTransactions, 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 diff --git a/tests/nip47_event_handlers_test.go b/tests/nip47_event_handlers_test.go deleted file mode 100644 index 197dee76e..000000000 --- a/tests/nip47_event_handlers_test.go +++ /dev/null @@ -1,962 +0,0 @@ -package tests - -import ( - "context" - "encoding/json" - "testing" - "time" - - "github.com/nbd-wtf/go-nostr" - "github.com/nbd-wtf/go-nostr/nip04" - "github.com/stretchr/testify/assert" - - "github.com/getAlby/nostr-wallet-connect/db" - "github.com/getAlby/nostr-wallet-connect/nip47" -) - -// TODO: split up this file! - -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" - } -} -` - -// 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 TestHandleEncryption(t *testing.T) {} - -func TestHandleMultiPayInvoiceEvent(t *testing.T) { - ctx := context.TODO() - defer removeTestService() - svc, err := createTestService() - 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.nip47Svc.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.nip47Svc.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.nip47Svc.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.nip47Svc.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 removeTestService() - svc, err := createTestService() - 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.nip47Svc.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.nip47Svc.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.nip47Svc.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 removeTestService() - svc, err := createTestService() - 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.nip47Svc.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.nip47Svc.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.nip47Svc.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 removeTestService() - svc, err := createTestService() - 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.nip47Svc.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.nip47Svc.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.nip47Svc.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.nip47Svc.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.nip47Svc.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.nip47Svc.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.nip47Svc.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 removeTestService() - svc, err := createTestService() - 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.nip47Svc.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.nip47Svc.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.nip47Svc.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 removeTestService() - svc, err := createTestService() - 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.nip47Svc.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.nip47Svc.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 removeTestService() - svc, err := createTestService() - 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.nip47Svc.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.nip47Svc.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 removeTestService() - svc, err := createTestService() - 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.nip47Svc.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.nip47Svc.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 removeTestService() - svc, err := createTestService() - 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.nip47Svc.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.nip47Svc.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) -} diff --git a/tests/nip47_notifier_test.go b/tests/nip47_notifier_test.go deleted file mode 100644 index f2c5078a3..000000000 --- a/tests/nip47_notifier_test.go +++ /dev/null @@ -1,135 +0,0 @@ -package tests - -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" - "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 removeTestService() - svc, err := createTestService() - assert.NoError(t, err) - - /*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) - - nip47NotificationQueue := nip47.NewNip47NotificationQueue() - svc.eventPublisher.RegisterSubscriber(nip47NotificationQueue) - - testEvent := &events.Event{ - Event: "nwc_payment_received", - Properties: &events.PaymentReceivedEventProperties{ - PaymentHash: mockPaymentHash, - Amount: uint64(mockTransaction.Amount), - NodeType: "LDK", - }, - } - - svc.eventPublisher.Publish(testEvent) - - receivedEvent := <-nip47NotificationQueue.Channel() - assert.Equal(t, testEvent, receivedEvent) - - relay := NewMockRelay() - - nip47Svc := nip47.NewNip47Service(svc.db, svc.eventPublisher, svc.cfg, svc.lnClient) - - notifier := nip47.NewNip47Notifier(nip47Svc, relay, svc.cfg, svc.db, 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 := 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 removeTestService() - svc, err := createTestService() - assert.NoError(t, err) - _, _, err = createApp(svc) - assert.NoError(t, err) - - nip47NotificationQueue := nip47.NewNip47NotificationQueue() - svc.eventPublisher.RegisterSubscriber(nip47NotificationQueue) - - testEvent := &events.Event{ - Event: "nwc_payment_received", - Properties: &events.PaymentReceivedEventProperties{ - PaymentHash: mockPaymentHash, - Amount: uint64(mockTransaction.Amount), - NodeType: "LDK", - }, - } - - svc.eventPublisher.Publish(testEvent) - - receivedEvent := <-nip47NotificationQueue.Channel() - assert.Equal(t, testEvent, receivedEvent) - - relay := NewMockRelay() - - nip47Svc := nip47.NewNip47Service(svc.db, svc.eventPublisher, svc.cfg, svc.lnClient) - - notifier := nip47.NewNip47Notifier(nip47Svc, relay, svc.cfg, svc.db, 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/tests/test_service.go b/tests/test_service.go new file mode 100644 index 000000000..ef1b33d58 --- /dev/null +++ b/tests/test_service.go @@ -0,0 +1,59 @@ +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" + "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, + ) + + cfg.Start("") + + eventPublisher := events.NewEventPublisher() + + return &TestService{ + Cfg: cfg, + LNClient: mockLn, + EventPublisher: eventPublisher, + DB: gormDb, + }, nil +} + +type TestService struct { + Cfg config.Config + LNClient lnclient.LNClient + EventPublisher events.EventPublisher + DB *gorm.DB +} + +func RemoveTestService() { + os.Remove(testDB) +} diff --git a/wails/wails_app.go b/wails/wails_app.go index 1da838f18..304ce91ac 100644 --- a/wails/wails_app.go +++ b/wails/wails_app.go @@ -6,25 +6,28 @@ import ( "log" "github.com/getAlby/nostr-wallet-connect/api" + "github.com/getAlby/nostr-wallet-connect/logger" "github.com/getAlby/nostr-wallet-connect/service" - "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" + "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()), + api: api.NewAPI(svc, svc.GetDB(), svc.GetConfig(), svc.GetEventPublisher()), + db: svc.GetDB(), } } @@ -35,8 +38,6 @@ func (app *WailsApp) startup(ctx context.Context) { } func LaunchWailsApp(app *WailsApp, assets embed.FS, appIcon []byte) { - logger := NewWailsLogger(app.logger.Logger) - err := wails.Run(&options.App{ Title: "AlbyHub", Width: 1024, @@ -44,6 +45,7 @@ func LaunchWailsApp(app *WailsApp, assets embed.FS, appIcon []byte) { 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}, @@ -67,40 +69,37 @@ func LaunchWailsApp(app *WailsApp, assets embed.FS, appIcon []byte) { } } -func NewWailsLogger(appLogger *logrus.Logger) WailsLogger { - return WailsLogger{ - AppLogger: appLogger, - } +func NewWailsLogger() WailsLogger { + return WailsLogger{} } type WailsLogger struct { - App } -func (logger WailsLogger) Print(message string) { - logger.AppLogger.Print(message) +func (wailsLogger WailsLogger) Print(message string) { + logger.Logger.Print(message) } -func (logger WailsLogger) Trace(message string) { - logger.AppLogger.Trace(message) +func (wailsLogger WailsLogger) Trace(message string) { + logger.Logger.Trace(message) } -func (logger WailsLogger) Debug(message string) { - logger.AppLogger.Debug(message) +func (wailsLogger WailsLogger) Debug(message string) { + logger.Logger.Debug(message) } -func (logger WailsLogger) Info(message string) { - logger.Applogger.Logger.Info(message) +func (wailsLogger WailsLogger) Info(message string) { + logger.Logger.Info(message) } -func (logger WailsLogger) Warning(message string) { - logger.Applogger.Logger.Warning(message) +func (wailsLogger WailsLogger) Warning(message string) { + logger.Logger.Warning(message) } -func (logger WailsLogger) Error(message string) { - logger.Applogger.Logger.Error(message) +func (wailsLogger WailsLogger) Error(message string) { + logger.Logger.Error(message) } -func (logger WailsLogger) Fatal(message string) { - logger.AppLogger.Fatal(message) +func (wailsLogger WailsLogger) Fatal(message string) { + logger.Logger.Fatal(message) } diff --git a/wails/wails_handlers.go b/wails/wails_handlers.go index a320a86aa..e8de2386a 100644 --- a/wails/wails_handlers.go +++ b/wails/wails_handlers.go @@ -12,6 +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/logger" "github.com/wailsapp/wails/v2/pkg/runtime" ) @@ -37,7 +38,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string err := app.svc.GetAlbyOAuthSvc().CallbackHandler(ctx, code) if err != nil { - app.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -57,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.GetDB().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"} @@ -66,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.logger.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()} } @@ -169,7 +171,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string case "/api/alby/me": me, err := app.svc.GetAlbyOAuthSvc().GetMe(ctx) if err != nil { - app.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -180,7 +182,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string case "/api/alby/balance": balance, err := app.svc.GetAlbyOAuthSvc().GetBalance(ctx) if err != nil { - app.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -194,7 +196,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string payRequest := &alby.AlbyPayRequest{} err := json.Unmarshal([]byte(body), payRequest) if err != nil { - app.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -218,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.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -235,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.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -269,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.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -316,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.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -358,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.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -406,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.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -416,7 +418,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string err = app.api.SetNextBackupReminder(backupReminderRequest) if err != nil { - app.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -428,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.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -438,7 +440,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string err = app.api.ChangeUnlockPassword(changeUnlockPasswordRequest) if err != nil { - app.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -450,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.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -459,7 +461,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string } err = app.api.Start(startRequest) if err != nil { - app.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -473,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.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -482,7 +484,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string } err = app.api.Setup(ctx, setupRequest) if err != nil { - app.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -494,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.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -503,7 +505,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string } sendPaymentProbesResponse, err := app.api.SendPaymentProbes(ctx, sendPaymentProbesRequest) if err != nil { - app.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -515,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.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -524,7 +526,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string } sendSpontaneousPaymentProbesResponse, err := app.api.SendSpontaneousPaymentProbes(ctx, sendSpontaneousPaymentProbesRequest) if err != nil { - app.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -536,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.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -549,7 +551,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string DefaultFilename: "nwc.bkp", }) if err != nil { - app.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -559,7 +561,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string backupFile, err := os.Create(saveFilePath) if err != nil { - app.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -572,7 +574,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string err = app.api.CreateBackup(backupRequest.UnlockPassword, backupFile) if err != nil { - app.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -584,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.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -597,7 +599,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string DefaultFilename: "nwc.bkp", }) if err != nil { - app.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -607,7 +609,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string backupFile, err := os.Open(backupFilePath) if err != nil { - app.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -619,7 +621,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string err = app.api.RestoreBackup(restoreRequest.UnlockPassword, backupFile) if err != nil { - app.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -637,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.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -646,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.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, "body": body, @@ -656,7 +658,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string return WailsRequestRouterResponse{Body: logOutputResponse, Error: ""} } - app.logger.Logger.WithFields(logrus.Fields{ + logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, }).Error("Unhandled route") From ffd0bae0b5e1b901b5a0e9daf616b054d274de0c Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Sun, 16 Jun 2024 20:59:46 +0700 Subject: [PATCH 05/16] fix: wails bindings import --- frontend/platform_specific/wails/src/utils/request.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 9c223e4d5030855ddc383afe608e4d0114a672a8 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Sun, 16 Jun 2024 21:31:31 +0700 Subject: [PATCH 06/16] chore: restructure service files --- service/profiler.go | 63 ++++++++++++++++ service/service.go | 176 ++++---------------------------------------- service/start.go | 87 +++++++++++++++++++--- service/stop.go | 34 +++++++++ 4 files changed, 189 insertions(+), 171 deletions(-) create mode 100644 service/profiler.go create mode 100644 service/stop.go 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 index beddbfc75..00261c401 100644 --- a/service/service.go +++ b/service/service.go @@ -2,20 +2,15 @@ package service import ( "context" + "time" - "errors" - "fmt" - "net/http" - "net/http/pprof" "os" - "path" "path/filepath" "strings" "sync" "github.com/adrg/xdg" "github.com/nbd-wtf/go-nostr" - "gopkg.in/DataDog/dd-trace-go.v1/profiler" "gorm.io/gorm" "github.com/joho/godotenv" @@ -28,12 +23,6 @@ import ( "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/nip47" "github.com/getAlby/nostr-wallet-connect/nip47/models" ) @@ -127,103 +116,6 @@ func NewService(ctx context.Context) (*service, error) { return svc, nil } -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 -} - -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 -} - func (svc *service) createFilters(identityPubkey string) nostr.Filters { filter := nostr.Filter{ Tags: nostr.TagMap{"p": []string{identityPubkey}}, @@ -236,10 +128,6 @@ func (svc *service) noticeHandler(notice string) { logger.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 { svc.nip47Service.StartNotifier(ctx, sub.Relay, svc.lnClient) @@ -304,56 +192,20 @@ func finishRestoreNode(workDir string) { } } -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 (svc *service) Shutdown() { + svc.StopLNClient() + svc.eventPublisher.Publish(&events.Event{ + Event: "nwc_stopped", + }) + // wait for any remaining events + time.Sleep(1 * time.Second) } -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()) +func (svc *service) StopApp() { + if svc.appCancelFn != nil { + svc.appCancelFn() + svc.wg.Wait() } - - go func() { - <-ctx.Done() - profiler.Stop() - }() } func (svc *service) GetDB() *gorm.DB { @@ -376,6 +228,10 @@ func (svc *service) GetEventPublisher() events.EventPublisher { return svc.eventPublisher } +func (svc *service) GetLNClient() lnclient.LNClient { + return svc.lnClient +} + 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 index cb5fc6290..c3ea4d65b 100644 --- a/service/start.go +++ b/service/start.go @@ -3,13 +3,22 @@ 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" ) @@ -127,19 +136,75 @@ func (svc *service) StartApp(encryptionKey string) error { return nil } -// TODO: remove and call StopLNClient() instead -func (svc *service) StopApp() { - if svc.appCancelFn != nil { - svc.appCancelFn() - svc.wg.Wait() +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) } -} -func (svc *service) Shutdown() { - svc.StopLNClient() svc.eventPublisher.Publish(&events.Event{ - Event: "nwc_stopped", + Event: "nwc_node_started", + Properties: map[string]interface{}{ + "node_type": lnBackend, + }, }) - // wait for any remaining events - time.Sleep(1 * time.Second) + 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 +} From b46a736aa7c8a98d5d942bcc9fb6a1266d1e3725 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Sun, 16 Jun 2024 22:36:35 +0700 Subject: [PATCH 07/16] chore: split up nip47 controller tests --- README.md | 4 + nip47/controllers/controller_test.go | 796 ------------------ .../get_balance_controller_test.go | 109 +++ nip47/controllers/get_info_controller_test.go | 114 +++ .../list_transactions_controller.go | 2 +- .../list_transactions_controller_test.go | 105 +++ .../controllers/lookup_invoice_controller.go | 2 +- .../lookup_invoice_controller_test.go | 99 +++ .../make_invoice_controller_test.go | 92 ++ .../multi_pay_invoice_controller_test.go | 117 +-- .../multi_pay_keysend_controller_test.go | 174 ++++ .../pay_invoice_controller_test.go | 138 +++ .../pay_keysend_controller_test.go | 102 +++ nip47/event_handler.go | 4 +- 14 files changed, 1006 insertions(+), 852 deletions(-) delete mode 100644 nip47/controllers/controller_test.go create mode 100644 nip47/controllers/get_balance_controller_test.go create mode 100644 nip47/controllers/get_info_controller_test.go create mode 100644 nip47/controllers/list_transactions_controller_test.go create mode 100644 nip47/controllers/lookup_invoice_controller_test.go create mode 100644 nip47/controllers/make_invoice_controller_test.go create mode 100644 nip47/controllers/multi_pay_keysend_controller_test.go create mode 100644 nip47/controllers/pay_invoice_controller_test.go create mode 100644 nip47/controllers/pay_keysend_controller_test.go diff --git a/README.md b/README.md index 77906b5c3..b82395a04 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,10 @@ _If you get a blank screen, try running in your normal terminal (outside of vsco $ go test ./... +#### Test matching regular expression + + $ go test ./... -run TestHandleGetInfoEvent + ### Profiling The application supports both the Go pprof library and the DataDog profiler. diff --git a/nip47/controllers/controller_test.go b/nip47/controllers/controller_test.go deleted file mode 100644 index e652b6d5c..000000000 --- a/nip47/controllers/controller_test.go +++ /dev/null @@ -1,796 +0,0 @@ -package controllers - -// TODO: split and re-enable tests - -// import ( -// "context" -// "encoding/json" -// "testing" - -// "github.com/nbd-wtf/go-nostr" -// "github.com/stretchr/testify/assert" - -// "github.com/getAlby/nostr-wallet-connect/nip47/models" -// "github.com/getAlby/nostr-wallet-connect/tests" -// ) - -// // TODO: split up this file! - -// 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 nip47PayWrongMethodJson = ` -// { -// "method": "get_balance", -// "params": { -// "invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" -// } -// } -// ` -// const nip47PayJsonNoInvoice = ` -// { -// "method": "pay_invoice", -// "params": { -// "something": "else" -// } -// } -// ` - -// func TestHandleMultiPayKeysendEvent(t *testing.T) { - -// ctx := context.TODO() -// defer tests.RemoveTestService() -// svc, err := tests.CreateTestService() -// assert.NoError(t, err) - -// app, ss, err := tests.CreateApp(svc) -// assert.NoError(t, err) - -// request := &models.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: models.REQUEST_KIND, -// PubKey: app.NostrPubkey, -// Content: payload, -// } - -// reqEvent.ID = "multi_pay_keysend_without_permission" -// requestEvent := &db.RequestEvent{ -// NostrId: reqEvent.ID, -// } - -// responses := []*models.Response{} -// dTags := []nostr.Tags{} - -// publishResponse := func(response *models.Response, tags nostr.Tags) { -// responses = append(responses, response) -// dTags = append(dTags, tags) -// } - -// svc.nip47Svc.HandleMultiPayKeysendEvent(ctx, request, requestEvent, app, publishResponse) - -// assert.Equal(t, 2, len(responses)) -// for i := 0; i < len(responses); i++ { -// assert.Equal(t, models.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 models.PAY_KEYSEND_METHOD, see -// // https://github.com/getAlby/nostr-wallet-connect/issues/189 -// 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 = "multi_pay_keysend_with_permission" -// requestEvent.NostrId = reqEvent.ID -// responses = []*models.Response{} -// dTags = []nostr.Tags{} -// svc.nip47Svc.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.(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 = []*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()) -// } - -// func TestHandleGetBalanceEvent(t *testing.T) { -// ctx := context.TODO() -// defer tests.RemoveTestService() -// svc, err := tests.CreateTestService() -// assert.NoError(t, err) - -// app, ss, err := tests.CreateApp(svc) -// assert.NoError(t, err) - -// request := &models.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: models.REQUEST_KIND, -// PubKey: app.NostrPubkey, -// Content: payload, -// } - -// reqEvent.ID = "test_get_balance_without_permission" -// requestEvent := &db.RequestEvent{ -// NostrId: reqEvent.ID, -// } - -// responses := []*models.Response{} - -// publishResponse := func(response *models.Response, tags nostr.Tags) { -// responses = append(responses, response) -// } - -// err = svc.DB.Create(&requestEvent).Error -// assert.NoError(t, err) - -// svc.nip47Svc.HandleGetBalanceEvent(ctx, request, requestEvent, app, publishResponse) - -// assert.Equal(t, responses[0].Error.Code, models.ERROR_RESTRICTED) - -// // with permission -// expiresAt := time.Now().Add(24 * time.Hour) -// appPermission := &db.AppPermission{ -// AppId: app.ID, -// App: *app, -// RequestMethod: models.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 = []*models.Response{} -// svc.nip47Svc.HandleGetBalanceEvent(ctx, request, requestEvent, app, publishResponse) - -// assert.Equal(t, responses[0].Result.(*getBalanceResponse).Balance, int64(21000)) - -// // 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, request, requestEvent, 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) -// } - -// func TestHandlePayInvoiceEvent(t *testing.T) { -// ctx := context.TODO() -// defer tests.RemoveTestService() -// svc, err := tests.CreateTestService() -// assert.NoError(t, err) - -// app, ss, err := tests.CreateApp(svc) -// assert.NoError(t, err) - -// request := &models.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: models.REQUEST_KIND, -// PubKey: app.NostrPubkey, -// Content: payload, -// } - -// reqEvent.ID = "pay_invoice_without_permission" -// requestEvent := &db.RequestEvent{ -// NostrId: reqEvent.ID, -// } - -// responses := []*models.Response{} - -// publishResponse := func(response *models.Response, tags nostr.Tags) { -// responses = append(responses, response) -// } - -// svc.nip47Svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - -// assert.Equal(t, models.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: models.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 = []*models.Response{} -// svc.nip47Svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - -// assert.Equal(t, responses[0].Result.(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 = []*models.Response{} -// svc.nip47Svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - -// assert.Equal(t, models.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 = []*models.Response{} -// svc.nip47Svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - -// assert.Equal(t, models.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 = []*models.Response{} -// svc.nip47Svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - -// assert.Equal(t, models.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 = []*models.Response{} -// svc.nip47Svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - -// assert.Equal(t, models.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 = []*models.Response{} -// svc.nip47Svc.HandlePayInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - -// assert.Equal(t, responses[0].Result.(payResponse).Preimage, "123preimage") -// } - -// func TestHandlePayKeysendEvent(t *testing.T) { -// ctx := context.TODO() -// defer tests.RemoveTestService() -// svc, err := tests.CreateTestService() -// assert.NoError(t, err) - -// app, ss, err := tests.CreateApp(svc) -// assert.NoError(t, err) - -// request := &models.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: models.REQUEST_KIND, -// PubKey: app.NostrPubkey, -// Content: payload, -// } - -// reqEvent.ID = "pay_keysend_without_permission" -// requestEvent := &db.RequestEvent{ -// NostrId: reqEvent.ID, -// } - -// responses := []*models.Response{} - -// publishResponse := func(response *models.Response, tags nostr.Tags) { -// responses = append(responses, response) -// } - -// svc.nip47Svc.HandlePayKeysendEvent(ctx, request, requestEvent, app, publishResponse) - -// assert.Equal(t, models.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 models.PAY_KEYSEND_METHOD, see -// // https://github.com/getAlby/nostr-wallet-connect/issues/189 -// 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 = "pay_keysend_with_permission" -// requestEvent.NostrId = reqEvent.ID -// responses = []*models.Response{} -// svc.nip47Svc.HandlePayKeysendEvent(ctx, request, requestEvent, app, publishResponse) - -// assert.Equal(t, responses[0].Result.(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 = []*models.Response{} -// svc.nip47Svc.HandlePayKeysendEvent(ctx, request, requestEvent, app, publishResponse) - -// assert.Equal(t, models.ERROR_QUOTA_EXCEEDED, responses[0].Error.Code) -// } - -// func TestHandleLookupInvoiceEvent(t *testing.T) { -// ctx := context.TODO() -// defer tests.RemoveTestService() -// svc, err := tests.CreateTestService() -// assert.NoError(t, err) - -// app, ss, err := tests.CreateApp(svc) -// assert.NoError(t, err) - -// request := &models.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: models.REQUEST_KIND, -// PubKey: app.NostrPubkey, -// Content: payload, -// } - -// reqEvent.ID = "test_lookup_invoice_without_permission" -// requestEvent := &db.RequestEvent{ -// NostrId: reqEvent.ID, -// } - -// responses := []*models.Response{} - -// publishResponse := func(response *models.Response, tags nostr.Tags) { -// responses = append(responses, response) -// } - -// svc.nip47Svc.HandleLookupInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - -// assert.Equal(t, models.ERROR_RESTRICTED, responses[0].Error.Code) - -// // with permission -// expiresAt := time.Now().Add(24 * time.Hour) -// appPermission := &db.AppPermission{ -// AppId: app.ID, -// App: *app, -// RequestMethod: models.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 = []*models.Response{} -// svc.nip47Svc.HandleLookupInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - -// transaction := responses[0].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) -// } - -// func TestHandleMakeInvoiceEvent(t *testing.T) { -// ctx := context.TODO() -// defer tests.RemoveTestService() -// svc, err := tests.CreateTestService() -// assert.NoError(t, err) - -// app, ss, err := tests.CreateApp(svc) -// assert.NoError(t, err) - -// request := &models.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: models.REQUEST_KIND, -// PubKey: app.NostrPubkey, -// Content: payload, -// } - -// reqEvent.ID = "test_make_invoice_without_permission" -// requestEvent := &db.RequestEvent{ -// NostrId: reqEvent.ID, -// } - -// responses := []*models.Response{} - -// publishResponse := func(response *models.Response, tags nostr.Tags) { -// responses = append(responses, response) -// } - -// svc.nip47Svc.HandleMakeInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - -// assert.Equal(t, models.ERROR_RESTRICTED, responses[0].Error.Code) - -// // with permission -// expiresAt := time.Now().Add(24 * time.Hour) -// appPermission := &db.AppPermission{ -// AppId: app.ID, -// App: *app, -// RequestMethod: models.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 = []*models.Response{} -// svc.nip47Svc.HandleMakeInvoiceEvent(ctx, request, requestEvent, app, publishResponse) - -// assert.Equal(t, tests.MockTransaction.Preimage, responses[0].Result.(*makeInvoiceResponse).Preimage) -// } - -// func TestHandleListTransactionsEvent(t *testing.T) { -// ctx := context.TODO() -// defer tests.RemoveTestService() -// svc, err := tests.CreateTestService() -// assert.NoError(t, err) - -// app, ss, err := tests.CreateApp(svc) -// assert.NoError(t, err) - -// request := &models.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: models.REQUEST_KIND, -// PubKey: app.NostrPubkey, -// Content: payload, -// } - -// reqEvent.ID = "test_list_transactions_without_permission" -// requestEvent := &db.RequestEvent{ -// NostrId: reqEvent.ID, -// } - -// responses := []*models.Response{} - -// publishResponse := func(response *models.Response, tags nostr.Tags) { -// responses = append(responses, response) -// } - -// svc.nip47Svc.HandleListTransactionsEvent(ctx, request, requestEvent, app, publishResponse) - -// assert.Equal(t, models.ERROR_RESTRICTED, responses[0].Error.Code) - -// // with permission -// expiresAt := time.Now().Add(24 * time.Hour) -// appPermission := &db.AppPermission{ -// AppId: app.ID, -// App: *app, -// RequestMethod: models.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 = []*models.Response{} -// svc.nip47Svc.HandleListTransactionsEvent(ctx, request, requestEvent, app, publishResponse) - -// assert.Equal(t, 2, len(responses[0].Result.(*listTransactionsResponse).Transactions)) -// transaction := responses[0].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) -// } - -// func TestHandleGetInfoEvent(t *testing.T) { -// ctx := context.TODO() -// defer tests.RemoveTestService() -// svc, err := tests.CreateTestService() -// assert.NoError(t, err) - -// app, ss, err := tests.CreateApp(svc) -// assert.NoError(t, err) - -// request := &models.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: models.REQUEST_KIND, -// PubKey: app.NostrPubkey, -// Content: payload, -// } - -// reqEvent.ID = "test_get_info_without_permission" -// requestEvent := &db.RequestEvent{ -// NostrId: reqEvent.ID, -// } - -// responses := []*models.Response{} - -// publishResponse := func(response *models.Response, tags nostr.Tags) { -// responses = append(responses, response) -// } - -// svc.nip47Svc.HandleGetInfoEvent(ctx, request, requestEvent, app, publishResponse) - -// assert.Equal(t, models.ERROR_RESTRICTED, responses[0].Error.Code) - -// expiresAt := time.Now().Add(24 * time.Hour) -// appPermission := &db.AppPermission{ -// AppId: app.ID, -// App: *app, -// RequestMethod: models.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 = []*models.Response{} -// svc.nip47Svc.HandleGetInfoEvent(ctx, request, requestEvent, app, publishResponse) - -// nodeInfo := responses[0].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/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_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 index 6e1db622f..e5d7c261a 100644 --- a/nip47/controllers/list_transactions_controller.go +++ b/nip47/controllers/list_transactions_controller.go @@ -27,7 +27,7 @@ type listTransactionsController struct { lnClient lnclient.LNClient } -func NewlistTransactionsController(lnClient lnclient.LNClient) *listTransactionsController { +func NewListTransactionsController(lnClient lnclient.LNClient) *listTransactionsController { return &listTransactionsController{ lnClient: lnClient, } 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 index a769ee1bb..924d127f3 100644 --- a/nip47/controllers/lookup_invoice_controller.go +++ b/nip47/controllers/lookup_invoice_controller.go @@ -26,7 +26,7 @@ type lookupInvoiceController struct { lnClient lnclient.LNClient } -func NewlookupInvoiceController(lnClient lnclient.LNClient) *lookupInvoiceController { +func NewLookupInvoiceController(lnClient lnclient.LNClient) *lookupInvoiceController { return &lookupInvoiceController{ lnClient: lnClient, } 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_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/multi_pay_invoice_controller_test.go b/nip47/controllers/multi_pay_invoice_controller_test.go index c0c0c7c7b..3e5c2a0a7 100644 --- a/nip47/controllers/multi_pay_invoice_controller_test.go +++ b/nip47/controllers/multi_pay_invoice_controller_test.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "testing" - "time" "github.com/nbd-wtf/go-nostr" "github.com/stretchr/testify/assert" @@ -77,8 +76,9 @@ func TestHandleMultiPayInvoiceEvent_NoPermission(t *testing.T) { responses := []*models.Response{} dTags := []nostr.Tags{} - requestEvent := &db.RequestEvent{} - svc.DB.Save(requestEvent) + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) checkPermission := func(amountMsat uint64) *models.Response { return &models.Response{ @@ -95,17 +95,17 @@ func TestHandleMultiPayInvoiceEvent_NoPermission(t *testing.T) { } NewMultiPayInvoiceController(svc.LNClient, svc.DB, svc.EventPublisher). - HandleMultiPayInvoiceEvent(ctx, nip47Request, requestEvent.ID, app, checkPermission, publishResponse) + 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) } } -// TODO: split up this test func TestHandleMultiPayInvoiceEvent_WithPermission(t *testing.T) { ctx := context.TODO() @@ -120,21 +120,6 @@ func TestHandleMultiPayInvoiceEvent_WithPermission(t *testing.T) { err = json.Unmarshal([]byte(nip47MultiPayJson), nip47Request) assert.NoError(t, err) - // with permission - maxAmount := 1000 - budgetRenewal := "never" - expiresAt := time.Now().Add(24 * time.Hour) - 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) - responses := []*models.Response{} dTags := []nostr.Tags{} @@ -146,54 +131,82 @@ func TestHandleMultiPayInvoiceEvent_WithPermission(t *testing.T) { checkPermission := func(amountMsat uint64) *models.Response { return nil } - requestEvent := &db.RequestEvent{} - svc.DB.Save(requestEvent) + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) NewMultiPayInvoiceController(svc.LNClient, svc.DB, svc.EventPublisher). - HandleMultiPayInvoiceEvent(ctx, nip47Request, requestEvent.ID, app, checkPermission, publishResponse) + HandleMultiPayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse) assert.Equal(t, 2, len(responses)) for i := 0; i < len(responses); i++ { - assert.Equal(t, responses[i].Result.(payResponse).Preimage, "123preimage") + 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) } - // one malformed invoice +} + +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{} + 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, responses[0].Error.Code, models.ERROR_INTERNAL) + 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, responses[1].Result.(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), 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()) + 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_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_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_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/event_handler.go b/nip47/event_handler.go index 959edb5a8..27b8f1089 100644 --- a/nip47/event_handler.go +++ b/nip47/event_handler.go @@ -306,11 +306,11 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio HandleMakeInvoiceEvent(ctx, nip47Request, requestEvent.ID, checkPermission, publishResponse) case models.LOOKUP_INVOICE_METHOD: controllers. - NewlookupInvoiceController(lnClient). + NewLookupInvoiceController(lnClient). HandleLookupInvoiceEvent(ctx, nip47Request, requestEvent.ID, checkPermission, publishResponse) case models.LIST_TRANSACTIONS_METHOD: controllers. - NewlistTransactionsController(lnClient). + NewListTransactionsController(lnClient). HandleListTransactionsEvent(ctx, nip47Request, requestEvent.ID, checkPermission, publishResponse) case models.GET_INFO_METHOD: controllers. From 78a93bcc00a007c9a1ccff2a40a0e68c68020c86 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Sun, 16 Jun 2024 23:46:59 +0700 Subject: [PATCH 08/16] chore: move nostr keys out of config --- alby/alby_oauth_service.go | 7 ++- api/api.go | 9 ++-- config/config.go | 35 ++------------ config/models.go | 3 -- http/http_service.go | 2 +- nip47/event_handler.go | 8 ++-- nip47/event_handler_test.go | 6 +-- nip47/nip47_service.go | 7 ++- nip47/notifications/nip47_notifier.go | 11 +++-- nip47/notifications/nip47_notifier_test.go | 4 +- nip47/publish_nip47_info.go | 4 +- service/keys/keys.go | 55 ++++++++++++++++++++++ service/models.go | 2 + service/service.go | 13 ++++- service/start.go | 12 ++--- tests/create_app.go | 2 +- tests/test_service.go | 6 ++- wails/wails_app.go | 2 +- 18 files changed, 119 insertions(+), 69 deletions(-) create mode 100644 service/keys/keys.go diff --git a/alby/alby_oauth_service.go b/alby/alby_oauth_service.go index 4d8db38c5..27ee75adc 100644 --- a/alby/alby_oauth_service.go +++ b/alby/alby_oauth_service.go @@ -21,12 +21,14 @@ import ( "github.com/getAlby/nostr-wallet-connect/events" "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 { cfg config.Config oauthConf *oauth2.Config db *gorm.DB + keys keys.Keys } const ( @@ -36,7 +38,7 @@ const ( userIdentifierKey = "AlbyUserIdentifier" ) -func NewAlbyOAuthService(db *gorm.DB, cfg config.Config) *albyOAuthService { +func NewAlbyOAuthService(db *gorm.DB, cfg config.Config, keys keys.Keys) *albyOAuthService { conf := &oauth2.Config{ ClientID: cfg.GetEnv().AlbyClientId, ClientSecret: cfg.GetEnv().AlbyClientSecret, @@ -58,6 +60,7 @@ func NewAlbyOAuthService(db *gorm.DB, cfg config.Config) *albyOAuthService { oauthConf: conf, cfg: cfg, db: db, + keys: keys, } return albyOAuthSvc } @@ -540,7 +543,7 @@ func (svc *albyOAuthService) createAlbyAccountNWCNode(ctx context.Context) (stri } createNodeRequest := createNWCNodeRequest{ - WalletPubkey: svc.cfg.GetNostrPublicKey(), + WalletPubkey: svc.keys.GetNostrPublicKey(), } body := bytes.NewBuffer([]byte{}) diff --git a/api/api.go b/api/api.go index 06b83cb1a..cc31cc726 100644 --- a/api/api.go +++ b/api/api.go @@ -23,6 +23,7 @@ import ( 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" ) @@ -32,15 +33,17 @@ type api struct { cfg config.Config svc service.Service permissionsSvc permissions.PermissionsService + keys keys.Keys } -func NewAPI(svc service.Service, gormDB *gorm.DB, config config.Config, eventsPublisher events.EventPublisher) *api { +func NewAPI(svc service.Service, gormDB *gorm.DB, config config.Config, keys keys.Keys, eventsPublisher events.EventPublisher) *api { return &api{ db: gormDB, dbSvc: db.NewDBService(gormDB), cfg: config, svc: svc, permissionsSvc: permissions.NewPermissionsService(gormDB, eventsPublisher), + keys: keys, } } @@ -82,7 +85,7 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons if err == nil { query := returnToUrl.Query() query.Add("relay", relayUrl) - query.Add("pubkey", api.cfg.GetNostrPublicKey()) + query.Add("pubkey", api.keys.GetNostrPublicKey()) // if user.LightningAddress != "" { // query.Add("lud16", user.LightningAddress) // } @@ -95,7 +98,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.cfg.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 } diff --git a/config/config.go b/config/config.go index 8712763dd..48773fbbb 100644 --- a/config/config.go +++ b/config/config.go @@ -9,17 +9,14 @@ import ( "github.com/getAlby/nostr-wallet-connect/db" "github.com/getAlby/nostr-wallet-connect/logger" - "github.com/nbd-wtf/go-nostr" "gorm.io/gorm" "gorm.io/gorm/clause" ) type config struct { - Env *AppConfig - CookieSecret string - NostrSecretKey string - NostrPublicKey string - db *gorm.DB + Env *AppConfig + CookieSecret string + db *gorm.DB } const ( @@ -84,14 +81,6 @@ func (cfg *config) init(env *AppConfig) { } } -func (cfg *config) GetNostrPublicKey() string { - return cfg.NostrPublicKey -} - -func (cfg *config) GetNostrSecretKey() string { - return cfg.NostrSecretKey -} - func (cfg *config) GetCookieSecret() string { return cfg.CookieSecret } @@ -216,24 +205,6 @@ func (cfg *config) Setup(encryptionKey string) { cfg.SetUpdate("UnlockPasswordCheck", unlockPasswordCheck, encryptionKey) } -// TODO: move Nostr out of config -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 { - logger.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/http/http_service.go b/http/http_service.go index 237ddcd31..a38d7f842 100644 --- a/http/http_service.go +++ b/http/http_service.go @@ -40,7 +40,7 @@ const ( func NewHttpService(svc service.Service, eventPublisher events.EventPublisher) *HttpService { return &HttpService{ - api: api.NewAPI(svc, svc.GetDB(), svc.GetConfig(), svc.GetEventPublisher()), + api: api.NewAPI(svc, svc.GetDB(), svc.GetConfig(), svc.GetKeys(), svc.GetEventPublisher()), albyHttpSvc: alby.NewAlbyHttpService(svc.GetAlbyOAuthSvc(), svc.GetConfig().GetEnv()), cfg: svc.GetConfig(), eventPublisher: eventPublisher, diff --git a/nip47/event_handler.go b/nip47/event_handler.go index 27b8f1089..b03fae8b3 100644 --- a/nip47/event_handler.go +++ b/nip47/event_handler.go @@ -26,7 +26,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio "eventKind": event.Kind, }).Info("Processing Event") - ss, err := nip04.ComputeSharedSecret(event.PubKey, svc.cfg.GetNostrSecretKey()) + ss, err := nip04.ComputeSharedSecret(event.PubKey, svc.keys.GetNostrSecretKey()) if err != nil { logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, @@ -140,7 +140,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio }).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()) + ss, err = nip04.ComputeSharedSecret(app.NostrPubkey, svc.keys.GetNostrSecretKey()) if err != nil { logger.Logger.WithFields(logrus.Fields{ "requestEventNostrId": event.ID, @@ -345,13 +345,13 @@ func (svc *nip47Service) CreateResponse(initialEvent *nostr.Event, content inter allTags = append(allTags, tags...) resp := &nostr.Event{ - PubKey: svc.cfg.GetNostrPublicKey(), + PubKey: svc.keys.GetNostrPublicKey(), CreatedAt: nostr.Now(), Kind: models.RESPONSE_KIND, Tags: allTags, Content: msg, } - err = resp.Sign(svc.cfg.GetNostrSecretKey()) + err = resp.Sign(svc.keys.GetNostrSecretKey()) if err != nil { return nil, err } diff --git a/nip47/event_handler_test.go b/nip47/event_handler_test.go index d5ba35f89..b1e874f69 100644 --- a/nip47/event_handler_test.go +++ b/nip47/event_handler_test.go @@ -32,7 +32,7 @@ func TestCreateResponse(t *testing.T) { reqEvent.ID = "12345" - ss, err := nip04.ComputeSharedSecret(reqPubkey, svc.Cfg.GetNostrSecretKey()) + ss, err := nip04.ComputeSharedSecret(reqPubkey, svc.Keys.GetNostrSecretKey()) assert.NoError(t, err) type dummyResponse struct { @@ -46,13 +46,13 @@ func TestCreateResponse(t *testing.T) { }, } - nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.EventPublisher) + 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.Cfg.GetNostrPublicKey(), res.PubKey) + assert.Equal(t, svc.Keys.GetNostrPublicKey(), res.PubKey) decrypted, err := nip04.Decrypt(res.Content, ss) assert.NoError(t, err) diff --git a/nip47/nip47_service.go b/nip47/nip47_service.go index ba1974303..06fa8fc99 100644 --- a/nip47/nip47_service.go +++ b/nip47/nip47_service.go @@ -8,6 +8,7 @@ import ( "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" ) @@ -16,6 +17,7 @@ type nip47Service struct { permissionsService permissions.PermissionsService nip47NotificationQueue notifications.Nip47NotificationQueue cfg config.Config + keys keys.Keys db *gorm.DB eventPublisher events.EventPublisher } @@ -27,7 +29,7 @@ type Nip47Service interface { CreateResponse(initialEvent *nostr.Event, content interface{}, tags nostr.Tags, ss []byte) (result *nostr.Event, err error) } -func NewNip47Service(db *gorm.DB, cfg config.Config, eventPublisher events.EventPublisher) *nip47Service { +func NewNip47Service(db *gorm.DB, cfg config.Config, keys keys.Keys, eventPublisher events.EventPublisher) *nip47Service { nip47NotificationQueue := notifications.NewNip47NotificationQueue() eventPublisher.RegisterSubscriber(nip47NotificationQueue) return &nip47Service{ @@ -36,11 +38,12 @@ func NewNip47Service(db *gorm.DB, cfg config.Config, eventPublisher events.Event 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.permissionsService, lnClient) + nip47Notifier := notifications.NewNip47Notifier(relay, svc.db, svc.cfg, svc.keys, svc.permissionsService, lnClient) go func() { for { select { diff --git a/nip47/notifications/nip47_notifier.go b/nip47/notifications/nip47_notifier.go index 2c25de708..da8eca929 100644 --- a/nip47/notifications/nip47_notifier.go +++ b/nip47/notifications/nip47_notifier.go @@ -12,6 +12,7 @@ import ( "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" @@ -25,18 +26,20 @@ type Relay interface { type Nip47Notifier struct { relay Relay cfg config.Config + keys keys.Keys lnClient lnclient.LNClient db *gorm.DB permissionsSvc permissions.PermissionsService } -func NewNip47Notifier(relay Relay, db *gorm.DB, cfg config.Config, permissionsSvc permissions.PermissionsService, lnClient lnclient.LNClient) *Nip47Notifier { +func NewNip47Notifier(relay Relay, db *gorm.DB, cfg config.Config, keys keys.Keys, permissionsSvc permissions.PermissionsService, lnClient lnclient.LNClient) *Nip47Notifier { return &Nip47Notifier{ relay: relay, cfg: cfg, db: db, lnClient: lnClient, permissionsSvc: permissionsSvc, + keys: keys, } } @@ -88,7 +91,7 @@ func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App "appId": app.ID, }).Info("Notifying subscriber") - ss, err := nip04.ComputeSharedSecret(app.NostrPubkey, notifier.cfg.GetNostrSecretKey()) + ss, err := nip04.ComputeSharedSecret(app.NostrPubkey, notifier.keys.GetNostrSecretKey()) if err != nil { logger.Logger.WithFields(logrus.Fields{ "notification": notification, @@ -118,13 +121,13 @@ func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App allTags = append(allTags, tags...) event := &nostr.Event{ - PubKey: notifier.cfg.GetNostrPublicKey(), + PubKey: notifier.keys.GetNostrPublicKey(), CreatedAt: nostr.Now(), Kind: models.NOTIFICATION_KIND, Tags: allTags, Content: msg, } - err = event.Sign(notifier.cfg.GetNostrSecretKey()) + err = event.Sign(notifier.keys.GetNostrSecretKey()) if err != nil { logger.Logger.WithFields(logrus.Fields{ "notification": notification, diff --git a/nip47/notifications/nip47_notifier_test.go b/nip47/notifications/nip47_notifier_test.go index 9d3f218a2..efb45a17b 100644 --- a/nip47/notifications/nip47_notifier_test.go +++ b/nip47/notifications/nip47_notifier_test.go @@ -58,7 +58,7 @@ func TestSendNotification(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) - notifier := NewNip47Notifier(relay, svc.DB, svc.Cfg, permissionsSvc, svc.LNClient) + notifier := NewNip47Notifier(relay, svc.DB, svc.Cfg, svc.Keys, permissionsSvc, svc.LNClient) notifier.ConsumeEvent(ctx, receivedEvent) assert.NotNil(t, relay.publishedEvent) @@ -115,7 +115,7 @@ func TestSendNotificationNoPermission(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) - notifier := NewNip47Notifier(relay, svc.DB, svc.Cfg, permissionsSvc, svc.LNClient) + notifier := NewNip47Notifier(relay, svc.DB, svc.Cfg, svc.Keys, permissionsSvc, svc.LNClient) notifier.ConsumeEvent(ctx, receivedEvent) assert.Nil(t, relay.publishedEvent) diff --git a/nip47/publish_nip47_info.go b/nip47/publish_nip47_info.go index d912cb263..a64fe689a 100644 --- a/nip47/publish_nip47_info.go +++ b/nip47/publish_nip47_info.go @@ -14,9 +14,9 @@ func (svc *nip47Service) PublishNip47Info(ctx context.Context, relay *nostr.Rela ev.Kind = models.INFO_EVENT_KIND ev.Content = models.CAPABILITIES ev.CreatedAt = nostr.Now() - ev.PubKey = svc.cfg.GetNostrPublicKey() + ev.PubKey = svc.keys.GetNostrPublicKey() ev.Tags = nostr.Tags{[]string{"notifications", notifications.NOTIFICATION_TYPES}} - err := ev.Sign(svc.cfg.GetNostrSecretKey()) + err := ev.Sign(svc.keys.GetNostrSecretKey()) if err != nil { return err } 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 58f6eeb04..8bdb7c3f4 100644 --- a/service/models.go +++ b/service/models.go @@ -5,6 +5,7 @@ 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/service/keys" "gorm.io/gorm" ) @@ -20,4 +21,5 @@ type Service interface { GetLNClient() lnclient.LNClient GetDB() *gorm.DB GetConfig() config.Config + GetKeys() keys.Keys } diff --git a/service/service.go b/service/service.go index 00261c401..9a7b7af59 100644 --- a/service/service.go +++ b/service/service.go @@ -19,6 +19,7 @@ import ( "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" @@ -38,6 +39,7 @@ type service struct { wg *sync.WaitGroup nip47Service nip47.Nip47Service appCancelFn context.CancelFunc + keys keys.Keys } func NewService(ctx context.Context) (*service, error) { @@ -88,15 +90,18 @@ func NewService(ctx context.Context) (*service, error) { 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), - nip47Service: nip47.NewNip47Service(gormDB, cfg, eventPublisher), + albyOAuthSvc: alby.NewAlbyOAuthService(gormDB, cfg, keys), + nip47Service: nip47.NewNip47Service(gormDB, cfg, keys, eventPublisher), db: gormDB, + keys: keys, } eventPublisher.RegisterSubscriber(svc.albyOAuthSvc) @@ -232,6 +237,10 @@ 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 index c3ea4d65b..17009f9d5 100644 --- a/service/start.go +++ b/service/start.go @@ -26,19 +26,19 @@ func (svc *service) StartNostr(ctx context.Context, encryptionKey string) error relayUrl := svc.cfg.GetRelayUrl() - err := svc.cfg.Start(encryptionKey) + err := svc.keys.Init(svc.cfg, encryptionKey) if err != nil { - logger.Logger.WithError(err).Fatal("Failed to start config") + logger.Logger.WithError(err).Fatal("Failed to init nostr keys") } - npub, err := nip19.EncodePublicKey(svc.cfg.GetNostrPublicKey()) + 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.cfg.GetNostrPublicKey(), + "hex": svc.keys.GetNostrPublicKey(), }).Info("Starting nostr-wallet-connect") svc.wg.Add(1) go func() { @@ -72,7 +72,7 @@ func (svc *service) StartNostr(ctx context.Context, encryptionKey string) error //connect to the relay logger.Logger.Infof("Connecting to the relay: %s", relayUrl) - relay, err := nostr.RelayConnect(ctx, relayUrl, nostr.WithNoticeHandler(svc.noticeHandler)) + relay, err = nostr.RelayConnect(ctx, relayUrl, nostr.WithNoticeHandler(svc.noticeHandler)) if err != nil { logger.Logger.WithError(err).Error("Failed to connect to relay") continue @@ -85,7 +85,7 @@ func (svc *service) StartNostr(ctx context.Context, encryptionKey string) error } logger.Logger.Info("Subscribing to events") - sub, err := relay.Subscribe(ctx, svc.createFilters(svc.cfg.GetNostrPublicKey())) + sub, err := relay.Subscribe(ctx, svc.createFilters(svc.keys.GetNostrPublicKey())) if err != nil { logger.Logger.WithError(err).Error("Failed to subscribe to events") continue diff --git a/tests/create_app.go b/tests/create_app.go index 0d20acbbf..7acb676a3 100644 --- a/tests/create_app.go +++ b/tests/create_app.go @@ -13,7 +13,7 @@ func CreateApp(svc *TestService) (app *db.App, ss []byte, err error) { return nil, nil, err } - ss, err = nip04.ComputeSharedSecret(svc.Cfg.GetNostrPublicKey(), senderPrivkey) + ss, err = nip04.ComputeSharedSecret(svc.Keys.GetNostrPublicKey(), senderPrivkey) if err != nil { return nil, nil, err } diff --git a/tests/test_service.go b/tests/test_service.go index ef1b33d58..2d3c480a7 100644 --- a/tests/test_service.go +++ b/tests/test_service.go @@ -8,6 +8,7 @@ import ( "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" ) @@ -35,7 +36,8 @@ func CreateTestService() (svc *TestService, err error) { gormDb, ) - cfg.Start("") + keys := keys.NewKeys() + keys.Init(cfg, "") eventPublisher := events.NewEventPublisher() @@ -44,10 +46,12 @@ func CreateTestService() (svc *TestService, err error) { LNClient: mockLn, EventPublisher: eventPublisher, DB: gormDb, + Keys: keys, }, nil } type TestService struct { + Keys keys.Keys Cfg config.Config LNClient lnclient.LNClient EventPublisher events.EventPublisher diff --git a/wails/wails_app.go b/wails/wails_app.go index 304ce91ac..0e5a54fd3 100644 --- a/wails/wails_app.go +++ b/wails/wails_app.go @@ -26,7 +26,7 @@ type WailsApp struct { func NewApp(svc service.Service) *WailsApp { return &WailsApp{ svc: svc, - api: api.NewAPI(svc, svc.GetDB(), svc.GetConfig(), svc.GetEventPublisher()), + api: api.NewAPI(svc, svc.GetDB(), svc.GetConfig(), svc.GetKeys(), svc.GetEventPublisher()), db: svc.GetDB(), } } From 114e054dcb5300473c7ea0fe88717c6c6bf5ebed Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 17 Jun 2024 00:04:28 +0700 Subject: [PATCH 09/16] chore: minor cleanup --- api/api.go | 14 ++++++++------ api/backup.go | 10 +++++----- http/http_service.go | 2 +- wails/wails_app.go | 2 +- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/api/api.go b/api/api.go index cc31cc726..b420698cc 100644 --- a/api/api.go +++ b/api/api.go @@ -34,9 +34,10 @@ type api struct { svc service.Service permissionsSvc permissions.PermissionsService keys keys.Keys + albyOAuthSvc alby.AlbyOAuthService } -func NewAPI(svc service.Service, gormDB *gorm.DB, config config.Config, keys keys.Keys, eventsPublisher events.EventPublisher) *api { +func NewAPI(svc service.Service, gormDB *gorm.DB, config config.Config, keys keys.Keys, albyOAuthSvc alby.AlbyOAuthService, eventsPublisher events.EventPublisher) *api { return &api{ db: gormDB, dbSvc: db.NewDBService(gormDB), @@ -44,6 +45,7 @@ func NewAPI(svc service.Service, gormDB *gorm.DB, config config.Config, keys key svc: svc, permissionsSvc: permissions.NewPermissionsService(gormDB, eventsPublisher), keys: keys, + albyOAuthSvc: albyOAuthSvc, } } @@ -276,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 { @@ -531,15 +533,15 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) { info.SetupCompleted = unlockPasswordCheck != "" info.Running = api.svc.GetLNClient() != nil info.BackendType = backendType - info.AlbyAuthUrl = api.svc.GetAlbyOAuthSvc().GetAuthUrl() + info.AlbyAuthUrl = api.albyOAuthSvc.GetAuthUrl() info.OAuthRedirect = !api.cfg.GetEnv().IsDefaultClientId() - albyUserIdentifier, err := api.svc.GetAlbyOAuthSvc().GetUserIdentifier() + albyUserIdentifier, err := api.albyOAuthSvc.GetUserIdentifier() if err != nil { 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 { @@ -619,7 +621,7 @@ func (api *api) Setup(ctx context.Context, setupRequest *SetupRequest) error { } if setupRequest.CashuMintUrl != "" { - api.svc.GetConfig().SetUpdate("CashuMintUrl", setupRequest.CashuMintUrl, setupRequest.UnlockPassword) + api.cfg.SetUpdate("CashuMintUrl", setupRequest.CashuMintUrl, setupRequest.UnlockPassword) } return nil diff --git a/api/backup.go b/api/backup.go index 9365416b6..b6b442802 100644 --- a/api/backup.go +++ b/api/backup.go @@ -25,11 +25,11 @@ import ( func (api *api) CreateBackup(unlockPassword string, w io.Writer) error { var err error - if !api.svc.GetConfig().CheckUnlockPassword(unlockPassword) { + if !api.cfg.CheckUnlockPassword(unlockPassword) { return errors.New("invalid unlock password") } - workDir, err := filepath.Abs(api.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) } @@ -105,7 +105,7 @@ func (api *api) CreateBackup(unlockPassword string, w io.Writer) error { } // Locate the main database file. - dbFilePath := api.svc.GetConfig().GetEnv().DatabaseUri + dbFilePath := api.cfg.GetEnv().DatabaseUri // Add the database file to the archive. logger.Logger.WithField("nwc.db", dbFilePath).Info("adding nwc db to zip") err = addFileToZip(dbFilePath, "nwc.db") @@ -134,12 +134,12 @@ func (api *api) CreateBackup(unlockPassword string, w io.Writer) error { } func (api *api) RestoreBackup(unlockPassword string, r io.Reader) error { - workDir, err := filepath.Abs(api.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) } - if strings.HasPrefix(api.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") } diff --git a/http/http_service.go b/http/http_service.go index a38d7f842..5fe946fe5 100644 --- a/http/http_service.go +++ b/http/http_service.go @@ -40,7 +40,7 @@ const ( func NewHttpService(svc service.Service, eventPublisher events.EventPublisher) *HttpService { return &HttpService{ - api: api.NewAPI(svc, svc.GetDB(), svc.GetConfig(), svc.GetKeys(), svc.GetEventPublisher()), + 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(), eventPublisher: eventPublisher, diff --git a/wails/wails_app.go b/wails/wails_app.go index 0e5a54fd3..de0652933 100644 --- a/wails/wails_app.go +++ b/wails/wails_app.go @@ -26,7 +26,7 @@ type WailsApp struct { func NewApp(svc service.Service) *WailsApp { return &WailsApp{ svc: svc, - api: api.NewAPI(svc, svc.GetDB(), svc.GetConfig(), svc.GetKeys(), svc.GetEventPublisher()), + api: api.NewAPI(svc, svc.GetDB(), svc.GetConfig(), svc.GetKeys(), svc.GetAlbyOAuthSvc(), svc.GetEventPublisher()), db: svc.GetDB(), } } From e7ca1a883702d9c3b45c1f45f8ae051fe66a6612 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 17 Jun 2024 12:13:40 +0700 Subject: [PATCH 10/16] chore: move http mode entrypoint into cmd --- .vscode/settings.json | 1 - README.md | 9 +++++---- main_http.go => cmd/http/main.go | 7 ------- main_wails.go | 2 -- 4 files changed, 5 insertions(+), 14 deletions(-) rename main_http.go => cmd/http/main.go (83%) 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/README.md b/README.md index b82395a04..595a08713 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ As data storage SQLite is used. 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) @@ -73,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 @@ -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/main_http.go b/cmd/http/main.go similarity index 83% rename from main_http.go rename to cmd/http/main.go index 29e088e8e..7a40fa0bd 100644 --- a/main_http.go +++ b/cmd/http/main.go @@ -1,8 +1,3 @@ -//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 ( @@ -22,8 +17,6 @@ import ( 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) diff --git a/main_wails.go b/main_wails.go index 6dc9fa0a8..143fb6444 100644 --- a/main_wails.go +++ b/main_wails.go @@ -19,8 +19,6 @@ var assets embed.FS //go:embed appicon.png var appIcon []byte -// ignore this warning: we use build tags -// this function will only be executed if the wails tag is set func main() { log.Info("NWC Starting in WAILS mode") ctx, cancel := context.WithCancel(context.Background()) From 45c35d0c7fbc9b0ca8477a28d9e6ba007fe6ab70 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 17 Jun 2024 12:14:03 +0700 Subject: [PATCH 11/16] fix: do not show lock button in wails mode --- frontend/src/components/layouts/AppLayout.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) 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

+
+ )}
); } From ffaed5db1171cf888c2396ae7cf188adc386bd2f Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 17 Jun 2024 12:28:58 +0700 Subject: [PATCH 12/16] chore: add wails field to wails logs --- wails/wails_app.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/wails/wails_app.go b/wails/wails_app.go index de0652933..001c109e3 100644 --- a/wails/wails_app.go +++ b/wails/wails_app.go @@ -77,29 +77,29 @@ type WailsLogger struct { } func (wailsLogger WailsLogger) Print(message string) { - logger.Logger.Print(message) + logger.Logger.WithField("wails", true).Print(message) } func (wailsLogger WailsLogger) Trace(message string) { - logger.Logger.Trace(message) + logger.Logger.WithField("wails", true).Trace(message) } func (wailsLogger WailsLogger) Debug(message string) { - logger.Logger.Debug(message) + logger.Logger.WithField("wails", true).Debug(message) } func (wailsLogger WailsLogger) Info(message string) { - logger.Logger.Info(message) + logger.Logger.WithField("wails", true).Info(message) } func (wailsLogger WailsLogger) Warning(message string) { - logger.Logger.Warning(message) + logger.Logger.WithField("wails", true).Warning(message) } func (wailsLogger WailsLogger) Error(message string) { - logger.Logger.Error(message) + logger.Logger.WithField("wails", true).Error(message) } func (wailsLogger WailsLogger) Fatal(message string) { - logger.Logger.Fatal(message) + logger.Logger.WithField("wails", true).Fatal(message) } From c125e12fab1d73e8327edfb1f81f60fb00d0fb43 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 17 Jun 2024 12:31:28 +0700 Subject: [PATCH 13/16] fix: test command in github actions --- .github/workflows/build-docker.yaml | 2 +- .github/workflows/multiplatform.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 From e8d9495514e81a2888f3df620a8a167c16bbbbe7 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 17 Jun 2024 12:35:55 +0700 Subject: [PATCH 14/16] fix: go build commands --- .github/workflows/package-raspberry-pi.yml | 2 +- Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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` ./ From 54128288edb749d69d4f00bd7729d2efd39b1e65 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 17 Jun 2024 13:04:41 +0700 Subject: [PATCH 15/16] fix: do not re-create router on each app re-render --- frontend/src/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ( <> From c13c66023616c1dd03aa182b3152d779db6bb494 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 17 Jun 2024 19:40:47 +0700 Subject: [PATCH 16/16] chore: add TODOs --- config/config.go | 1 + db/migrations/202403171120_delete_ldk_payments.go | 1 + nip47/controllers/get_balance_controller.go | 1 + nip47/event_handler.go | 1 + 4 files changed, 4 insertions(+) diff --git a/config/config.go b/config/config.go index 48773fbbb..0fad5cd46 100644 --- a/config/config.go +++ b/config/config.go @@ -201,6 +201,7 @@ 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) } diff --git a/db/migrations/202403171120_delete_ldk_payments.go b/db/migrations/202403171120_delete_ldk_payments.go index 00daae3a4..c4dcf9387 100644 --- a/db/migrations/202403171120_delete_ldk_payments.go +++ b/db/migrations/202403171120_delete_ldk_payments.go @@ -7,6 +7,7 @@ import ( "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. diff --git a/nip47/controllers/get_balance_controller.go b/nip47/controllers/get_balance_controller.go index 8b278bb85..a9a4f8880 100644 --- a/nip47/controllers/get_balance_controller.go +++ b/nip47/controllers/get_balance_controller.go @@ -30,6 +30,7 @@ func NewGetBalanceController(lnClient lnclient.LNClient) *getBalanceController { } } +// 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) diff --git a/nip47/event_handler.go b/nip47/event_handler.go index b03fae8b3..405450111 100644 --- a/nip47/event_handler.go +++ b/nip47/event_handler.go @@ -279,6 +279,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio "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.