diff --git a/alby/alby_oauth_service.go b/alby/alby_oauth_service.go
index ad9c5a6b0..dc330ea19 100644
--- a/alby/alby_oauth_service.go
+++ b/alby/alby_oauth_service.go
@@ -253,9 +253,20 @@ func (svc *albyOAuthService) GetInfo(ctx context.Context) (*AlbyInfo, error) {
LatestReleaseNotes string `json:"latest_release_notes"`
}
+ type albyInfoIncident struct {
+ Name string `json:"name"`
+ Started string `json:"started"`
+ Status string `json:"status"`
+ Impact string `json:"impact"`
+ Url string `json:"url"`
+ }
+
type albyInfo struct {
- Hub albyInfoHub `json:"hub"`
- // TODO: consider getting healthcheck/incident info and showing in the hub
+ Hub albyInfoHub `json:"hub"`
+ Status string `json:"status"`
+ Healthy bool `json:"healthy"`
+ AccountAvailable bool `json:"account_available"` // false if country is blocked (can still use Alby Hub without an Alby Account)
+ Incidents []albyInfoIncident `json:"incidents"`
}
body, err := io.ReadAll(res.Body)
@@ -279,11 +290,26 @@ func (svc *albyOAuthService) GetInfo(ctx context.Context) (*AlbyInfo, error) {
return nil, err
}
+ incidents := []AlbyInfoIncident{}
+ for _, incident := range info.Incidents {
+ incidents = append(incidents, AlbyInfoIncident{
+ Name: incident.Name,
+ Started: incident.Started,
+ Status: incident.Status,
+ Impact: incident.Impact,
+ Url: incident.Url,
+ })
+ }
+
return &AlbyInfo{
Hub: AlbyInfoHub{
LatestVersion: info.Hub.LatestVersion,
LatestReleaseNotes: info.Hub.LatestReleaseNotes,
},
+ Status: info.Status,
+ Healthy: info.Healthy,
+ AccountAvailable: info.AccountAvailable,
+ Incidents: incidents,
}, nil
}
@@ -702,8 +728,13 @@ func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Eve
}
if event.Event == "nwc_backup_channels" {
- if err := svc.backupChannels(ctx, event); err != nil {
- logger.Logger.WithError(err).Error("Failed to backup channels")
+ // if backup fails, try again (max 3 attempts)
+ for i := 0; i < 3; i++ {
+ if err := svc.backupChannels(ctx, event); err != nil {
+ logger.Logger.WithField("attempt", i).WithError(err).Error("Failed to backup channels")
+ continue
+ }
+ break
}
return
}
diff --git a/alby/models.go b/alby/models.go
index a0bdfe946..5a50df000 100644
--- a/alby/models.go
+++ b/alby/models.go
@@ -55,9 +55,20 @@ type AlbyInfoHub struct {
LatestReleaseNotes string `json:"latestReleaseNotes"`
}
+type AlbyInfoIncident struct {
+ Name string `json:"name"`
+ Started string `json:"started"`
+ Status string `json:"status"`
+ Impact string `json:"impact"`
+ Url string `json:"url"`
+}
+
type AlbyInfo struct {
- Hub AlbyInfoHub `json:"hub"`
- // TODO: consider getting healthcheck/incident info and showing in the hub
+ Hub AlbyInfoHub `json:"hub"`
+ Status string `json:"status"`
+ Healthy bool `json:"healthy"`
+ AccountAvailable bool `json:"accountAvailable"` // false if country is blocked (can still use Alby Hub without an Alby Account)
+ Incidents []AlbyInfoIncident `json:"incidents"`
}
type AlbyMeHub struct {
diff --git a/api/api.go b/api/api.go
index 0e111fd52..414db536e 100644
--- a/api/api.go
+++ b/api/api.go
@@ -402,6 +402,7 @@ func (api *api) ListChannels(ctx context.Context) ([]Channel, error) {
Id: channel.Id,
RemotePubkey: channel.RemotePubkey,
FundingTxId: channel.FundingTxId,
+ FundingTxVout: channel.FundingTxVout,
Active: channel.Active,
Public: channel.Public,
InternalChannel: channel.InternalChannel,
@@ -441,7 +442,15 @@ func (api *api) ChangeUnlockPassword(changeUnlockPasswordRequest *ChangeUnlockPa
return errors.New("LNClient not started")
}
- err := api.cfg.ChangeUnlockPassword(changeUnlockPasswordRequest.CurrentUnlockPassword, changeUnlockPasswordRequest.NewUnlockPassword)
+ autoUnlockPassword, err := api.cfg.Get("AutoUnlockPassword", "")
+ if err != nil {
+ return err
+ }
+ if autoUnlockPassword != "" {
+ return errors.New("Please disable auto-unlock before using this feature")
+ }
+
+ err = api.cfg.ChangeUnlockPassword(changeUnlockPasswordRequest.CurrentUnlockPassword, changeUnlockPasswordRequest.NewUnlockPassword)
if err != nil {
logger.Logger.WithError(err).Error("failed to change unlock password")
@@ -453,6 +462,21 @@ func (api *api) ChangeUnlockPassword(changeUnlockPasswordRequest *ChangeUnlockPa
return api.Stop()
}
+func (api *api) SetAutoUnlockPassword(unlockPassword string) error {
+ if api.svc.GetLNClient() == nil {
+ return errors.New("LNClient not started")
+ }
+
+ err := api.cfg.SetAutoUnlockPassword(unlockPassword)
+
+ if err != nil {
+ logger.Logger.WithError(err).Error("failed to set auto unlock password")
+ return err
+ }
+
+ return nil
+}
+
func (api *api) Stop() error {
if !startMutex.TryLock() {
// do not allow to stop twice in case this is somehow called twice
@@ -686,6 +710,7 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) {
info := InfoResponse{}
backendType, _ := api.cfg.Get("LNBackendType", "")
ldkVssEnabled, _ := api.cfg.Get("LdkVssEnabled", "")
+ autoUnlockPassword, _ := api.cfg.Get("AutoUnlockPassword", "")
info.SetupCompleted = api.cfg.SetupCompleted()
if api.startupError != nil {
info.StartupError = api.startupError.Error()
@@ -699,6 +724,8 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) {
info.EnableAdvancedSetup = api.cfg.GetEnv().EnableAdvancedSetup
info.LdkVssEnabled = ldkVssEnabled == "true"
info.VssSupported = backendType == config.LDKBackendType && api.cfg.GetEnv().LDKVssUrl != ""
+ info.AutoUnlockPasswordEnabled = autoUnlockPassword != ""
+ info.AutoUnlockPasswordSupported = api.cfg.GetEnv().IsDefaultClientId()
albyUserIdentifier, err := api.albyOAuthSvc.GetUserIdentifier()
if err != nil {
logger.Logger.WithError(err).Error("Failed to get alby user identifier")
diff --git a/api/backup.go b/api/backup.go
index 3532bddfe..f4adc04cb 100644
--- a/api/backup.go
+++ b/api/backup.go
@@ -30,6 +30,14 @@ func (api *api) CreateBackup(unlockPassword string, w io.Writer) error {
return errors.New("invalid unlock password")
}
+ autoUnlockPassword, err := api.cfg.Get("AutoUnlockPassword", "")
+ if err != nil {
+ return err
+ }
+ if autoUnlockPassword != "" {
+ return errors.New("Please disable auto-unlock before using this feature")
+ }
+
workDir, err := filepath.Abs(api.cfg.GetEnv().Workdir)
if err != nil {
return fmt.Errorf("failed to get absolute workdir: %w", err)
diff --git a/api/models.go b/api/models.go
index 401d360b7..a668696b8 100644
--- a/api/models.go
+++ b/api/models.go
@@ -21,6 +21,7 @@ type API interface {
GetChannelPeerSuggestions(ctx context.Context) ([]alby.ChannelPeerSuggestion, error)
ResetRouter(key string) error
ChangeUnlockPassword(changeUnlockPasswordRequest *ChangeUnlockPasswordRequest) error
+ SetAutoUnlockPassword(unlockPassword string) error
Stop() error
GetNodeConnectionInfo(ctx context.Context) (*lnclient.NodeConnectionInfo, error)
GetNodeStatus(ctx context.Context) (*lnclient.NodeStatus, error)
@@ -71,7 +72,7 @@ type App struct {
BudgetUsage uint64 `json:"budgetUsage"`
BudgetRenewal string `json:"budgetRenewal"`
Isolated bool `json:"isolated"`
- Balance uint64 `json:"balance"`
+ Balance int64 `json:"balance"`
Metadata Metadata `json:"metadata,omitempty"`
}
@@ -159,22 +160,24 @@ type User struct {
}
type InfoResponse struct {
- BackendType string `json:"backendType"`
- SetupCompleted bool `json:"setupCompleted"`
- OAuthRedirect bool `json:"oauthRedirect"`
- Running bool `json:"running"`
- Unlocked bool `json:"unlocked"`
- AlbyAuthUrl string `json:"albyAuthUrl"`
- NextBackupReminder string `json:"nextBackupReminder"`
- AlbyUserIdentifier string `json:"albyUserIdentifier"`
- AlbyAccountConnected bool `json:"albyAccountConnected"`
- Version string `json:"version"`
- Network string `json:"network"`
- EnableAdvancedSetup bool `json:"enableAdvancedSetup"`
- LdkVssEnabled bool `json:"ldkVssEnabled"`
- VssSupported bool `json:"vssSupported"`
- StartupError string `json:"startupError"`
- StartupErrorTime time.Time `json:"startupErrorTime"`
+ BackendType string `json:"backendType"`
+ SetupCompleted bool `json:"setupCompleted"`
+ OAuthRedirect bool `json:"oauthRedirect"`
+ Running bool `json:"running"`
+ Unlocked bool `json:"unlocked"`
+ AlbyAuthUrl string `json:"albyAuthUrl"`
+ NextBackupReminder string `json:"nextBackupReminder"`
+ AlbyUserIdentifier string `json:"albyUserIdentifier"`
+ AlbyAccountConnected bool `json:"albyAccountConnected"`
+ Version string `json:"version"`
+ Network string `json:"network"`
+ EnableAdvancedSetup bool `json:"enableAdvancedSetup"`
+ LdkVssEnabled bool `json:"ldkVssEnabled"`
+ VssSupported bool `json:"vssSupported"`
+ StartupError string `json:"startupError"`
+ StartupErrorTime time.Time `json:"startupErrorTime"`
+ AutoUnlockPasswordSupported bool `json:"autoUnlockPasswordSupported"`
+ AutoUnlockPasswordEnabled bool `json:"autoUnlockPasswordEnabled"`
}
type MnemonicRequest struct {
@@ -189,6 +192,9 @@ type ChangeUnlockPasswordRequest struct {
CurrentUnlockPassword string `json:"currentUnlockPassword"`
NewUnlockPassword string `json:"newUnlockPassword"`
}
+type AutoUnlockRequest struct {
+ UnlockPassword string `json:"unlockPassword"`
+}
type ConnectPeerRequest = lnclient.ConnectPeerRequest
type OpenChannelRequest = lnclient.OpenChannelRequest
@@ -342,6 +348,7 @@ type Channel struct {
Id string `json:"id"`
RemotePubkey string `json:"remotePubkey"`
FundingTxId string `json:"fundingTxId"`
+ FundingTxVout uint32 `json:"fundingTxVout"`
Active bool `json:"active"`
Public bool `json:"public"`
InternalChannel interface{} `json:"internalChannel"`
diff --git a/config/config.go b/config/config.go
index 5739ba2f3..2d4c65eaf 100644
--- a/config/config.go
+++ b/config/config.go
@@ -248,6 +248,20 @@ func (cfg *config) ChangeUnlockPassword(currentUnlockPassword string, newUnlockP
return nil
}
+func (cfg *config) SetAutoUnlockPassword(unlockPassword string) error {
+ if unlockPassword != "" && !cfg.CheckUnlockPassword(unlockPassword) {
+ return errors.New("incorrect password")
+ }
+
+ err := cfg.SetUpdate("AutoUnlockPassword", unlockPassword, "")
+ if err != nil {
+ logger.Logger.WithError(err).Error("failed to update auto unlock password")
+ return err
+ }
+
+ return nil
+}
+
func (cfg *config) CheckUnlockPassword(encryptionKey string) bool {
decryptedValue, err := cfg.Get("UnlockPasswordCheck", encryptionKey)
diff --git a/config/models.go b/config/models.go
index e3e167eff..4f6bdea90 100644
--- a/config/models.go
+++ b/config/models.go
@@ -14,37 +14,38 @@ const (
)
type AppConfig struct {
- Relay string `envconfig:"RELAY" default:"wss://relay.getalby.com/v1"`
- LNBackendType string `envconfig:"LN_BACKEND_TYPE"`
- LNDAddress string `envconfig:"LND_ADDRESS"`
- LNDCertFile string `envconfig:"LND_CERT_FILE"`
- LNDMacaroonFile string `envconfig:"LND_MACAROON_FILE"`
- Workdir string `envconfig:"WORK_DIR"`
- Port string `envconfig:"PORT" default:"8080"`
- DatabaseUri string `envconfig:"DATABASE_URI" default:"nwc.db"`
- JWTSecret string `envconfig:"JWT_SECRET"`
- LogLevel string `envconfig:"LOG_LEVEL" default:"4"`
- LogToFile bool `envconfig:"LOG_TO_FILE" default:"true"`
- LDKNetwork string `envconfig:"LDK_NETWORK" default:"bitcoin"`
- LDKEsploraServer string `envconfig:"LDK_ESPLORA_SERVER" default:"https://electrs.getalbypro.com"` // TODO: remove LDK prefix
- LDKGossipSource string `envconfig:"LDK_GOSSIP_SOURCE"`
- LDKLogLevel string `envconfig:"LDK_LOG_LEVEL" default:"3"`
- LDKVssUrl string `envconfig:"LDK_VSS_URL"`
- LDKListeningAddresses string `envconfig:"LDK_LISTENING_ADDRESSES" default:"0.0.0.0:9735,[::]:9735"`
- MempoolApi string `envconfig:"MEMPOOL_API" default:"https://mempool.space/api"`
- AlbyClientId string `envconfig:"ALBY_OAUTH_CLIENT_ID" default:"J2PbXS1yOf"`
- AlbyClientSecret string `envconfig:"ALBY_OAUTH_CLIENT_SECRET" default:"rABK2n16IWjLTZ9M1uKU"`
- BaseUrl string `envconfig:"BASE_URL"`
- FrontendUrl string `envconfig:"FRONTEND_URL"`
- LogEvents bool `envconfig:"LOG_EVENTS" default:"true"`
- AutoLinkAlbyAccount bool `envconfig:"AUTO_LINK_ALBY_ACCOUNT" default:"true"`
- PhoenixdAddress string `envconfig:"PHOENIXD_ADDRESS"`
- PhoenixdAuthorization string `envconfig:"PHOENIXD_AUTHORIZATION"`
- GoProfilerAddr string `envconfig:"GO_PROFILER_ADDR"`
- DdProfilerEnabled bool `envconfig:"DD_PROFILER_ENABLED" default:"false"`
- EnableAdvancedSetup bool `envconfig:"ENABLE_ADVANCED_SETUP" default:"true"`
- AutoUnlockPassword string `envconfig:"AUTO_UNLOCK_PASSWORD"`
- LogDBQueries bool `envconfig:"LOG_DB_QUERIES" default:"false"`
+ Relay string `envconfig:"RELAY" default:"wss://relay.getalby.com/v1"`
+ LNBackendType string `envconfig:"LN_BACKEND_TYPE"`
+ LNDAddress string `envconfig:"LND_ADDRESS"`
+ LNDCertFile string `envconfig:"LND_CERT_FILE"`
+ LNDMacaroonFile string `envconfig:"LND_MACAROON_FILE"`
+ Workdir string `envconfig:"WORK_DIR"`
+ Port string `envconfig:"PORT" default:"8080"`
+ DatabaseUri string `envconfig:"DATABASE_URI" default:"nwc.db"`
+ JWTSecret string `envconfig:"JWT_SECRET"`
+ LogLevel string `envconfig:"LOG_LEVEL" default:"4"`
+ LogToFile bool `envconfig:"LOG_TO_FILE" default:"true"`
+ LDKNetwork string `envconfig:"LDK_NETWORK" default:"bitcoin"`
+ LDKEsploraServer string `envconfig:"LDK_ESPLORA_SERVER" default:"https://electrs.getalbypro.com"` // TODO: remove LDK prefix
+ LDKGossipSource string `envconfig:"LDK_GOSSIP_SOURCE"`
+ LDKLogLevel string `envconfig:"LDK_LOG_LEVEL" default:"3"`
+ LDKVssUrl string `envconfig:"LDK_VSS_URL"`
+ LDKListeningAddresses string `envconfig:"LDK_LISTENING_ADDRESSES" default:"0.0.0.0:9735,[::]:9735"`
+ LDKTransientNetworkGraph bool `envconfig:"LDK_TRANSIENT_NETWORK_GRAPH" default:"false"`
+ MempoolApi string `envconfig:"MEMPOOL_API" default:"https://mempool.space/api"`
+ AlbyClientId string `envconfig:"ALBY_OAUTH_CLIENT_ID" default:"J2PbXS1yOf"`
+ AlbyClientSecret string `envconfig:"ALBY_OAUTH_CLIENT_SECRET" default:"rABK2n16IWjLTZ9M1uKU"`
+ BaseUrl string `envconfig:"BASE_URL"`
+ FrontendUrl string `envconfig:"FRONTEND_URL"`
+ LogEvents bool `envconfig:"LOG_EVENTS" default:"true"`
+ AutoLinkAlbyAccount bool `envconfig:"AUTO_LINK_ALBY_ACCOUNT" default:"true"`
+ PhoenixdAddress string `envconfig:"PHOENIXD_ADDRESS"`
+ PhoenixdAuthorization string `envconfig:"PHOENIXD_AUTHORIZATION"`
+ GoProfilerAddr string `envconfig:"GO_PROFILER_ADDR"`
+ DdProfilerEnabled bool `envconfig:"DD_PROFILER_ENABLED" default:"false"`
+ EnableAdvancedSetup bool `envconfig:"ENABLE_ADVANCED_SETUP" default:"true"`
+ AutoUnlockPassword string `envconfig:"AUTO_UNLOCK_PASSWORD"`
+ LogDBQueries bool `envconfig:"LOG_DB_QUERIES" default:"false"`
}
func (c *AppConfig) IsDefaultClientId() bool {
@@ -60,6 +61,7 @@ type Config interface {
GetEnv() *AppConfig
CheckUnlockPassword(password string) bool
ChangeUnlockPassword(currentUnlockPassword string, newUnlockPassword string) error
+ SetAutoUnlockPassword(unlockPassword string) error
SaveUnlockPasswordCheck(encryptionKey string) error
SetupCompleted() bool
}
diff --git a/constants/constants.go b/constants/constants.go
index 98a8afeed..620f19d9d 100644
--- a/constants/constants.go
+++ b/constants/constants.go
@@ -47,5 +47,6 @@ const (
ERROR_RESTRICTED = "RESTRICTED"
ERROR_BAD_REQUEST = "BAD_REQUEST"
ERROR_NOT_FOUND = "NOT_FOUND"
+ ERROR_UNSUPPORTED_VERSION = "UNSUPPORTED_VERSION"
ERROR_OTHER = "OTHER"
)
diff --git a/db/queries/get_isolated_balance.go b/db/queries/get_isolated_balance.go
index bd9cef701..331b598e7 100644
--- a/db/queries/get_isolated_balance.go
+++ b/db/queries/get_isolated_balance.go
@@ -5,9 +5,9 @@ import (
"gorm.io/gorm"
)
-func GetIsolatedBalance(tx *gorm.DB, appId uint) uint64 {
+func GetIsolatedBalance(tx *gorm.DB, appId uint) int64 {
var received struct {
- Sum uint64
+ Sum int64
}
tx.
Table("transactions").
@@ -15,7 +15,7 @@ func GetIsolatedBalance(tx *gorm.DB, appId uint) uint64 {
Where("app_id = ? AND type = ? AND state = ?", appId, constants.TRANSACTION_TYPE_INCOMING, constants.TRANSACTION_STATE_SETTLED).Scan(&received)
var spent struct {
- Sum uint64
+ Sum int64
}
tx.
diff --git a/db/queries/get_isolated_balance_test.go b/db/queries/get_isolated_balance_test.go
new file mode 100644
index 000000000..773e5e0da
--- /dev/null
+++ b/db/queries/get_isolated_balance_test.go
@@ -0,0 +1,69 @@
+package queries
+
+import (
+ "testing"
+
+ "github.com/getAlby/hub/constants"
+ "github.com/getAlby/hub/db"
+ "github.com/getAlby/hub/tests"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestGetIsolatedBalance_PendingNoOverflow(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ require.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+ app.Isolated = true
+ svc.DB.Save(&app)
+
+ paymentAmount := uint64(1000) // 1 sat
+
+ tx := db.Transaction{
+ AppId: &app.ID,
+ RequestEventId: nil,
+ Type: constants.TRANSACTION_TYPE_OUTGOING,
+ State: constants.TRANSACTION_STATE_PENDING,
+ FeeReserveMsat: uint64(10000),
+ AmountMsat: paymentAmount,
+ PaymentRequest: tests.MockInvoice,
+ PaymentHash: tests.MockPaymentHash,
+ SelfPayment: true,
+ }
+ svc.DB.Save(&tx)
+
+ balance := GetIsolatedBalance(svc.DB, app.ID)
+ assert.Equal(t, int64(-11000), balance)
+}
+
+func TestGetIsolatedBalance_SettledNoOverflow(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ require.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+ app.Isolated = true
+ svc.DB.Save(&app)
+
+ paymentAmount := uint64(1000) // 1 sat
+
+ tx := db.Transaction{
+ AppId: &app.ID,
+ RequestEventId: nil,
+ Type: constants.TRANSACTION_TYPE_OUTGOING,
+ State: constants.TRANSACTION_STATE_SETTLED,
+ FeeReserveMsat: uint64(0),
+ AmountMsat: paymentAmount,
+ PaymentRequest: tests.MockInvoice,
+ PaymentHash: tests.MockPaymentHash,
+ SelfPayment: true,
+ }
+ svc.DB.Save(&tx)
+
+ balance := GetIsolatedBalance(svc.DB, app.ID)
+ assert.Equal(t, int64(-1000), balance)
+}
diff --git a/doc/logo.svg b/doc/logo.svg
index 5f913d238..e351a3130 100644
--- a/doc/logo.svg
+++ b/doc/logo.svg
@@ -1,17 +1,62 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/assets/suggested-apps/clams.png b/frontend/src/assets/suggested-apps/clams.png
new file mode 100644
index 000000000..42c976b17
Binary files /dev/null and b/frontend/src/assets/suggested-apps/clams.png differ
diff --git a/frontend/src/assets/suggested-apps/simple-boost.png b/frontend/src/assets/suggested-apps/simple-boost.png
new file mode 100644
index 000000000..b06005e00
Binary files /dev/null and b/frontend/src/assets/suggested-apps/simple-boost.png differ
diff --git a/frontend/src/assets/zapplanner/bitcoinbrink.png b/frontend/src/assets/zapplanner/bitcoinbrink.png
new file mode 100644
index 000000000..c3c370e98
Binary files /dev/null and b/frontend/src/assets/zapplanner/bitcoinbrink.png differ
diff --git a/frontend/src/assets/zapplanner/hrf.png b/frontend/src/assets/zapplanner/hrf.png
new file mode 100644
index 000000000..6449fc83d
Binary files /dev/null and b/frontend/src/assets/zapplanner/hrf.png differ
diff --git a/frontend/src/assets/zapplanner/opensats.png b/frontend/src/assets/zapplanner/opensats.png
new file mode 100644
index 000000000..b5a8a9a70
Binary files /dev/null and b/frontend/src/assets/zapplanner/opensats.png differ
diff --git a/frontend/src/components/SuggestedAppData.tsx b/frontend/src/components/SuggestedAppData.tsx
index c2747b4d9..51fd7e85d 100644
--- a/frontend/src/components/SuggestedAppData.tsx
+++ b/frontend/src/components/SuggestedAppData.tsx
@@ -4,6 +4,7 @@ import albyGo from "src/assets/suggested-apps/alby-go.png";
import alby from "src/assets/suggested-apps/alby.png";
import amethyst from "src/assets/suggested-apps/amethyst.png";
import buzzpay from "src/assets/suggested-apps/buzzpay.png";
+import clams from "src/assets/suggested-apps/clams.png";
import damus from "src/assets/suggested-apps/damus.png";
import hablanews from "src/assets/suggested-apps/habla-news.png";
import kiwi from "src/assets/suggested-apps/kiwi.png";
@@ -12,6 +13,7 @@ import nostrudel from "src/assets/suggested-apps/nostrudel.png";
import nostur from "src/assets/suggested-apps/nostur.png";
import paperScissorsHodl from "src/assets/suggested-apps/paper-scissors-hodl.png";
import primal from "src/assets/suggested-apps/primal.png";
+import simpleboost from "src/assets/suggested-apps/simple-boost.png";
import snort from "src/assets/suggested-apps/snort.png";
import stackernews from "src/assets/suggested-apps/stacker-news.png";
import uncleJim from "src/assets/suggested-apps/uncle-jim.png";
@@ -61,6 +63,13 @@ export const suggestedApps: SuggestedApp[] = [
internal: true,
logo: buzzpay,
},
+ {
+ id: "simpleboost",
+ title: "SimpleBoost",
+ description: "Donation widget for your website",
+ internal: true,
+ logo: simpleboost,
+ },
{
id: "alby-extension",
title: "Alby Extension",
@@ -520,6 +529,56 @@ export const suggestedApps: SuggestedApp[] = [
>
),
},
+ {
+ id: "clams",
+ title: "Clams",
+ description: "Multi wallet accounting tool",
+ webLink: "https://clams.tech/",
+ logo: clams,
+ guide: (
+ <>
+
+
In Clams
+
+
+ 1. Download and open{" "}
+
+ Clams
+ {" "}
+ on your device
+
+ 2. Add a connection: "+ Add Connection" → NWC
+
+
+
+
In Alby Hub
+
+
+ {" "}
+ 3. Click{" "}
+
+ Connect to Clams
+
+
+ 4. Set wallet permissions (Read Only)
+
+
+
+
In Clams
+
+ 5. Add label & paste connection secret
+ 6. Click Connect and Save
+
+
+ >
+ ),
+ },
{
id: "nostrudel",
title: "noStrudel",
@@ -647,65 +706,7 @@ export const suggestedApps: SuggestedApp[] = [
description: "Schedule payments",
webLink: "https://zapplanner.albylabs.com/",
logo: zapplanner,
- guide: (
- <>
-
-
In ZapPlanner
-
-
- 1. Open{" "}
-
- ZapPlanner
- {" "}
- in your browser
-
-
- 2. Click on{" "}
-
- New Recurring Payment
- {" "}
- → add the details and click{" "}
- Continue
-
-
- 3. Choose{" "}
-
- Nostr Wallet Connect URL
-
-
-
-
-
-
In Alby Hub
-
-
- 4. Click{" "}
-
- Connect to ZapPlanner
-
-
- 5. Set app's wallet permissions (full access recommended)
-
-
-
-
In ZapPlanner
-
-
- 6. Paste the connection secret from Alby Hub and click{" "}
-
- Create Recurring Payment
-
-
-
-
- >
- ),
+ internal: true,
},
{
id: "zapplepay",
@@ -1212,7 +1213,7 @@ export const suggestedApps: SuggestedApp[] = [
playLink:
"https://play.google.com/store/apps/details?id=com.getalby.mobile",
appleLink: "https://apps.apple.com/us/app/alby-go/id6471335774",
- zapStoreLink: "https://zapstore.dev",
+ zapStoreLink: "https://zapstore.dev/download/",
logo: albyGo,
guide: (
<>
diff --git a/frontend/src/components/TransactionItem.tsx b/frontend/src/components/TransactionItem.tsx
index 310fd9f08..f55e35857 100644
--- a/frontend/src/components/TransactionItem.tsx
+++ b/frontend/src/components/TransactionItem.tsx
@@ -72,7 +72,7 @@ function TransactionItem({ tx }: Props) {
tx.state === "failed"
? "bg-red-100 dark:bg-red-950"
: tx.state === "pending"
- ? "bg-gray-100 dark:bg-gray-950"
+ ? "bg-blue-100 dark:bg-blue-900"
: type === "outgoing"
? "bg-orange-100 dark:bg-orange-950"
: "bg-green-100 dark:bg-emerald-950"
@@ -85,7 +85,7 @@ function TransactionItem({ tx }: Props) {
tx.state === "failed"
? "stroke-rose-400 dark:stroke-red-600"
: tx.state === "pending"
- ? "stroke-gray-400 dark:stroke-gray-600"
+ ? "stroke-blue-500"
: type === "outgoing"
? "stroke-orange-400 dark:stroke-amber-600"
: "stroke-green-400 dark:stroke-emerald-500"
diff --git a/frontend/src/components/channels/ChannelDropdownMenu.tsx b/frontend/src/components/channels/ChannelDropdownMenu.tsx
index e5aa65a88..436e7f24b 100644
--- a/frontend/src/components/channels/ChannelDropdownMenu.tsx
+++ b/frontend/src/components/channels/ChannelDropdownMenu.tsx
@@ -49,7 +49,7 @@ export function ChannelDropdownMenu({
diff --git a/frontend/src/components/connections/AppCard.tsx b/frontend/src/components/connections/AppCard.tsx
index 75335da71..0fe3a5ffd 100644
--- a/frontend/src/components/connections/AppCard.tsx
+++ b/frontend/src/components/connections/AppCard.tsx
@@ -16,9 +16,10 @@ dayjs.extend(relativeTime);
type Props = {
app: App;
+ actions?: React.ReactNode;
};
-export default function AppCard({ app }: Props) {
+export default function AppCard({ app, actions }: Props) {
const navigate = useNavigate();
return (
@@ -34,6 +35,10 @@ export default function AppCard({ app }: Props) {
{app.name}
+ {!!actions && (
+ // stop the above navigation click handler
+ e.stopPropagation()}>{actions}
+ )}
diff --git a/frontend/src/components/icons/AlbyHubLogo.tsx b/frontend/src/components/icons/AlbyHubLogo.tsx
index 48a1783c8..ac8a02056 100644
--- a/frontend/src/components/icons/AlbyHubLogo.tsx
+++ b/frontend/src/components/icons/AlbyHubLogo.tsx
@@ -3,64 +3,67 @@ import { SVGAttributes } from "react";
export function AlbyHubLogo(props: SVGAttributes) {
return (
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
);
}
diff --git a/frontend/src/components/layouts/SettingsLayout.tsx b/frontend/src/components/layouts/SettingsLayout.tsx
index 4d69eb17b..94b267296 100644
--- a/frontend/src/components/layouts/SettingsLayout.tsx
+++ b/frontend/src/components/layouts/SettingsLayout.tsx
@@ -98,6 +98,9 @@ export default function SettingsLayout() {
Theme
+ {info?.autoUnlockPasswordSupported && (
+ Auto Unlock
+ )}
Unlock Password
diff --git a/frontend/src/hooks/useOnchainAddress.ts b/frontend/src/hooks/useOnchainAddress.ts
index 64bb0c31c..d1dee0f09 100644
--- a/frontend/src/hooks/useOnchainAddress.ts
+++ b/frontend/src/hooks/useOnchainAddress.ts
@@ -27,6 +27,7 @@ export function useOnchainAddress() {
throw new Error("No address in response");
}
swr.mutate(address, false);
+ return address;
} catch (error) {
toast({
variant: "destructive",
diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx
index 3ec6aeb1c..033841b6b 100644
--- a/frontend/src/routes.tsx
+++ b/frontend/src/routes.tsx
@@ -36,12 +36,15 @@ import { FirstChannel } from "src/screens/channels/first/FirstChannel";
import { OpenedFirstChannel } from "src/screens/channels/first/OpenedFirstChannel";
import { OpeningFirstChannel } from "src/screens/channels/first/OpeningFirstChannel";
import { BuzzPay } from "src/screens/internal-apps/BuzzPay";
+import { SimpleBoost } from "src/screens/internal-apps/SimpleBoost";
import { UncleJim } from "src/screens/internal-apps/UncleJim";
+import { ZapPlanner } from "src/screens/internal-apps/ZapPlanner";
import BuyBitcoin from "src/screens/onchain/BuyBitcoin";
import DepositBitcoin from "src/screens/onchain/DepositBitcoin";
import ConnectPeer from "src/screens/peers/ConnectPeer";
import Peers from "src/screens/peers/Peers";
import { AlbyAccount } from "src/screens/settings/AlbyAccount";
+import { AutoUnlock } from "src/screens/settings/AutoUnlock";
import { ChangeUnlockPassword } from "src/screens/settings/ChangeUnlockPassword";
import DebugTools from "src/screens/settings/DebugTools";
import DeveloperSettings from "src/screens/settings/DeveloperSettings";
@@ -168,6 +171,11 @@ const routes = [
index: true,
element: ,
},
+ {
+ path: "auto-unlock",
+ element: ,
+ handle: { crumb: () => "Auto Unlock" },
+ },
{
path: "change-unlock-password",
element: ,
@@ -235,6 +243,14 @@ const routes = [
path: "buzzpay",
element: ,
},
+ {
+ path: "simpleboost",
+ element: ,
+ },
+ {
+ path: "zapplanner",
+ element: ,
+ },
],
},
{
diff --git a/frontend/src/screens/BackupNode.tsx b/frontend/src/screens/BackupNode.tsx
index 232c3f6df..74c51f58a 100644
--- a/frontend/src/screens/BackupNode.tsx
+++ b/frontend/src/screens/BackupNode.tsx
@@ -44,8 +44,8 @@ export function BackupNode() {
}),
});
- if (!response?.ok) {
- throw new Error(`Error:${response?.statusText}`);
+ if (!response.ok) {
+ throw new Error(await response.text());
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
@@ -90,6 +90,14 @@ export function BackupNode() {
you create this backup, do not restart Alby Hub on this device.
+
+
+ Migration requires a fresh Alby Hub
+
+ To import the migration file, you must have a brand new Alby Hub on
+ another device and use the "Advanced" option in the onboarding.
+
+
What Happens Next
diff --git a/frontend/src/screens/Home.tsx b/frontend/src/screens/Home.tsx
index c4e0fb8d1..c7dc6384e 100644
--- a/frontend/src/screens/Home.tsx
+++ b/frontend/src/screens/Home.tsx
@@ -1,10 +1,10 @@
import { ExternalLinkIcon } from "lucide-react";
import { Link } from "react-router-dom";
-import albyGo from "src/assets/suggested-apps/alby-go.png";
import AppHeader from "src/components/AppHeader";
import ExternalLink from "src/components/ExternalLink";
import { AlbyHead } from "src/components/images/AlbyHead";
import Loading from "src/components/Loading";
+import { Badge } from "src/components/ui/badge";
import { Button } from "src/components/ui/button";
import {
Card,
@@ -18,6 +18,9 @@ import { useBalances } from "src/hooks/useBalances";
import { useInfo } from "src/hooks/useInfo";
import OnboardingChecklist from "src/screens/wallet/OnboardingChecklist";
+import albyGo from "src/assets/suggested-apps/alby-go.png";
+import zapplanner from "src/assets/suggested-apps/zapplanner.png";
+
function getGreeting(name: string | undefined) {
const hours = new Date().getHours();
let greeting;
@@ -81,6 +84,34 @@ function Home() {
)}
+
+
+
+
+
+
+
+
+
+
+ ZapPlanner NEW
+
+
+
+ Schedule automatic recurring lightning payments.
+
+
+
+
+
+ Open
+
+
+
+
@@ -102,10 +133,7 @@ function Home() {
-
- Learn more
-
-
+ Open
diff --git a/frontend/src/screens/apps/ShowApp.tsx b/frontend/src/screens/apps/ShowApp.tsx
index 59e3a5646..cf1934490 100644
--- a/frontend/src/screens/apps/ShowApp.tsx
+++ b/frontend/src/screens/apps/ShowApp.tsx
@@ -14,7 +14,7 @@ import {
import { handleRequestError } from "src/utils/handleRequestError";
import { request } from "src/utils/request"; // build the project for this to appear
-import { PencilIcon } from "lucide-react";
+import { PencilIcon, Trash2 } from "lucide-react";
import AppAvatar from "src/components/AppAvatar";
import AppHeader from "src/components/AppHeader";
import { IsolatedAppTopupDialog } from "src/components/IsolatedAppTopupDialog";
@@ -188,7 +188,9 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) {
contentRight={
- Delete
+
+
+
@@ -272,6 +274,14 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) {
: "Never"}
+ {app.metadata && (
+
+ Metadata
+
+ {JSON.stringify(app.metadata, null, 4)}
+
+
+ )}
diff --git a/frontend/src/screens/appstore/AppStoreDetail.tsx b/frontend/src/screens/appstore/AppStoreDetail.tsx
index 75e86b568..132f5d623 100644
--- a/frontend/src/screens/appstore/AppStoreDetail.tsx
+++ b/frontend/src/screens/appstore/AppStoreDetail.tsx
@@ -82,10 +82,10 @@ export function AppStoreDetail() {
)}
{app.zapStoreLink && (
-
+
- zap.store
+ Zapstore
)}
diff --git a/frontend/src/screens/channels/Channels.tsx b/frontend/src/screens/channels/Channels.tsx
index ff80cc26f..e5d43802e 100644
--- a/frontend/src/screens/channels/Channels.tsx
+++ b/frontend/src/screens/channels/Channels.tsx
@@ -1,12 +1,14 @@
import {
AlertTriangle,
- Bitcoin,
+ ArrowRight,
ChevronDown,
CopyIcon,
+ ExternalLinkIcon,
Heart,
Hotel,
HourglassIcon,
InfoIcon,
+ LinkIcon,
Settings2,
Unplug,
ZapIcon,
@@ -33,6 +35,14 @@ import {
CardHeader,
CardTitle,
} from "src/components/ui/card.tsx";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "src/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
@@ -42,6 +52,9 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "src/components/ui/dropdown-menu.tsx";
+import { Input } from "src/components/ui/input";
+import { Label } from "src/components/ui/label";
+import { LoadingButton } from "src/components/ui/loading-button";
import { CircleProgress } from "src/components/ui/progress.tsx";
import {
Tooltip,
@@ -59,10 +72,12 @@ import { useBalances } from "src/hooks/useBalances.ts";
import { useChannels } from "src/hooks/useChannels";
import { useIsDesktop } from "src/hooks/useMediaQuery.ts";
import { useNodeConnectionInfo } from "src/hooks/useNodeConnectionInfo.ts";
+import { useOnchainAddress } from "src/hooks/useOnchainAddress";
import { useSyncWallet } from "src/hooks/useSyncWallet.ts";
import { copyToClipboard } from "src/lib/clipboard.ts";
import { cn } from "src/lib/utils.ts";
-import { Channel, Node } from "src/types";
+import { Channel, CreateInvoiceRequest, Node, Transaction } from "src/types";
+import { openLink } from "src/utils/openLink";
import { request } from "src/utils/request";
export default function Channels() {
@@ -73,6 +88,12 @@ export default function Channels() {
const { data: albyBalance, mutate: reloadAlbyBalance } = useAlbyBalance();
const navigate = useNavigate();
const [nodes, setNodes] = React.useState([]);
+ const [swapInAmount, setSwapInAmount] = React.useState("");
+ const [swapOutAmount, setSwapOutAmount] = React.useState("");
+ const [swapOutDialogOpen, setSwapOutDialogOpen] = React.useState(false);
+ const [swapInDialogOpen, setSwapInDialogOpen] = React.useState(false);
+ const [loadingSwap, setLoadingSwap] = React.useState(false);
+ const { getNewAddress } = useOnchainAddress();
const { toast } = useToast();
const isDesktop = useIsDesktop();
@@ -104,6 +125,42 @@ export default function Channels() {
loadNodeStats();
}, [loadNodeStats]);
+ function openSwapOutDialog() {
+ setSwapOutAmount(
+ Math.floor(
+ ((findChannelWithLargestBalance("localSpendableBalance")
+ ?.localSpendableBalance || 0) *
+ 0.9) /
+ 1000
+ ).toString()
+ );
+ setSwapOutDialogOpen(true);
+ }
+ function openSwapInDialog() {
+ setSwapInAmount(
+ Math.floor(
+ ((findChannelWithLargestBalance("remoteBalance")?.remoteBalance || 0) *
+ 0.9) /
+ 1000
+ ).toString()
+ );
+ setSwapInDialogOpen(true);
+ }
+
+ function findChannelWithLargestBalance(
+ balanceType: "remoteBalance" | "localSpendableBalance"
+ ): Channel | undefined {
+ if (!channels || channels.length === 0) {
+ return undefined;
+ }
+
+ return channels.reduce((prevLargest, current) => {
+ return current[balanceType] > prevLargest[balanceType]
+ ? current
+ : prevLargest;
+ }, channels[0]);
+ }
+
const showHostedBalance =
albyBalance && albyBalance.sats > ALBY_HIDE_HOSTED_BALANCE_LIMIT;
@@ -114,6 +171,122 @@ export default function Channels() {
description="Manage your lightning node liquidity."
contentRight={
+ {/* TODO: move these dialogs to a new file */}
+
+
+
+ Swap out funds
+
+ Funds from one of your channels will be sent to your
+ on-chain balance via a swap service. This helps restore your
+ inbound liquidity.
+
+
+
+
Amount (sats)
+
+
setSwapOutAmount(e.target.value)}
+ />
+
+ The amount is set to 90% of the funds held in the channel
+ with the most outbound capacity.
+
+
+
+
+ {
+ setLoadingSwap(true);
+ const onchainAddress = await getNewAddress();
+ if (onchainAddress) {
+ openLink(
+ `https://boltz.exchange/?sendAsset=LN&receiveAsset=BTC&sendAmount=${swapOutAmount}&destination=${onchainAddress}&ref=alby`
+ );
+ }
+ setLoadingSwap(false);
+ }}
+ >
+ Swap out
+
+
+
+
+
+
+
+ Swap in funds
+
+ Swap on-chain funds into your lightning channels via a swap
+ service, increasing your spending balance using on-chain
+ funds from{" "}
+
+ your hub
+ {" "}
+ or an external wallet.
+
+
+
+
Amount (sats)
+
+
setSwapInAmount(e.target.value)}
+ />
+
+ The amount is set to 90% of the funds held by the
+ counterparty in the channel with the most receiving
+ capacity.
+
+
+
+
+ {
+ setLoadingSwap(true);
+ try {
+ const transaction = await request(
+ "/api/invoices",
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ amount: parseInt(swapInAmount) * 1000,
+ description: "Boltz Swap In",
+ } as CreateInvoiceRequest),
+ }
+ );
+ if (!transaction) {
+ throw new Error("no transaction in response");
+ }
+ openLink(
+ `https://boltz.exchange/?sendAsset=BTC&receiveAsset=LN&sendAmount=${swapInAmount}&destination=${transaction.invoice}&ref=alby`
+ );
+ } catch (error) {
+ toast({
+ variant: "destructive",
+ title: "Failed to generate swap invoice",
+ description: "" + error,
+ });
+ }
+ setLoadingSwap(false);
+ }}
+ >
+ Swap in
+
+
+
+
{isDesktop ? (
@@ -179,6 +352,32 @@ export default function Channels() {
)}
+
+ Swaps
+
+
+ Swap in
+
+
+
+ Swap out
+
+
+
Management
@@ -194,6 +393,7 @@ export default function Channels() {
+
Open Channel
@@ -361,8 +561,7 @@ export default function Channels() {
On-Chain
-
-
+
@@ -422,10 +621,33 @@ export default function Channels() {
{new Intl.NumberFormat().format(
balances.onchain.pendingBalancesFromChannelClosures
)}{" "}
- sats pending from one or more closed channels. Once spendable again
- these will become available in your on-chain balance. Funds from
- channels that were force closed may take up to 2 weeks to become
- available.{" "}
+ sats pending from closed channels with
+ {balances.onchain.pendingBalancesDetails.map((details, index) => (
+
+
+
+ {nodes.find((node) => node.public_key === details.nodeId)
+ ?.alias || "Unknown"}
+
+ {" "}
+ ({new Intl.NumberFormat().format(details.amount)} sats)
+
+ funding tx
+
+
+ {index < balances.onchain.pendingBalancesDetails.length - 1 &&
+ ","}
+
+ ))}
+ . Once spendable again these will become available in your on-chain
+ balance. Funds from channels that were force closed may take up to 2
+ weeks to become available.{" "}
{
+ if (nwcUri) {
+ setScriptContent(`
+Boost $1.00 `);
+ }
+ }, [nwcUri]);
+
+ if (!apps) {
+ return ;
+ }
+
+ const handleSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+ setLoading(true);
+ (async () => {
+ try {
+ if (apps?.some((existingApp) => existingApp.name === name)) {
+ throw new Error("A connection with the same name already exists.");
+ }
+
+ const createAppResponse = await createApp({
+ name,
+ scopes: ["lookup_invoice", "make_invoice"],
+ isolated: true,
+ metadata: {
+ app_store_app_id: "simpleboost",
+ },
+ });
+
+ setNwcUri(createAppResponse.pairingUri);
+
+ toast({ title: "Simple Boost connection created" });
+ } catch (error) {
+ handleRequestError(toast, "Failed to create connection", error);
+ }
+ setLoading(false);
+ })();
+ };
+
+ return (
+
+
+ {nwcUri && (
+
+
+ How to Add Widget
+
+
+
+ Paste the following code into an HTML block on your website.
+
+
+
+
+ By default the SimpleBoost widget is loaded from a CDN. See other
+ options{" "}
+
+ here
+
+
+
+
+ )}
+ {!nwcUri && (
+ <>
+
+
+ SimpleBoost is a donation button for your website. Add the widget
+ to your website and allow your visitors to send sats with the
+ click of a button.
+
+
+ ⚡ Lightning fast transactions directly to your Alby Hub
+ 🔗 Set amounts in Bitcoin or any other currency
+
+ 🔒 No lightning address required, secure read-only connection
+
+ 🤙 Can be paid from any lightning wallet
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/frontend/src/screens/internal-apps/ZapPlanner.tsx b/frontend/src/screens/internal-apps/ZapPlanner.tsx
new file mode 100644
index 000000000..55f309c29
--- /dev/null
+++ b/frontend/src/screens/internal-apps/ZapPlanner.tsx
@@ -0,0 +1,372 @@
+import React from "react";
+import AppHeader from "src/components/AppHeader";
+import AppCard from "src/components/connections/AppCard";
+import {
+ Card,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "src/components/ui/card";
+import { useToast } from "src/components/ui/use-toast";
+import { useApps } from "src/hooks/useApps";
+import { createApp } from "src/requests/createApp";
+import { CreateAppRequest, UpdateAppRequest } from "src/types";
+import { handleRequestError } from "src/utils/handleRequestError";
+
+import { LightningAddress } from "@getalby/lightning-tools";
+import { ExternalLinkIcon, PlusCircle } from "lucide-react";
+import alby from "src/assets/suggested-apps/alby.png";
+import bitcoinbrink from "src/assets/zapplanner/bitcoinbrink.png";
+import hrf from "src/assets/zapplanner/hrf.png";
+import opensats from "src/assets/zapplanner/opensats.png";
+import ExternalLink from "src/components/ExternalLink";
+import { Button, ExternalLinkButton } from "src/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "src/components/ui/dialog";
+import { Input } from "src/components/ui/input";
+import { Label } from "src/components/ui/label";
+import { LoadingButton } from "src/components/ui/loading-button";
+import { Textarea } from "src/components/ui/textarea";
+import { request } from "src/utils/request";
+
+type Recipient = {
+ name: string;
+ description: string;
+ lightningAddress: string;
+ logo?: string;
+};
+
+const recipients: Recipient[] = [
+ {
+ name: "Alby",
+ logo: alby,
+ description:
+ "Support the open-source development of Hub, Go, Lightning Browser Extension, developer tools and open protocols.",
+ lightningAddress: "hello@getalby.com",
+ },
+ {
+ name: "HRF",
+ description:
+ "We collaborate with transformative activists to develop innovative solutions that bring the world together in the fight against tyranny.",
+ lightningAddress: "hrf@btcpay.hrf.org",
+ logo: hrf,
+ },
+ {
+ name: "OpenSats",
+ description:
+ "Help us to provide sustainable funding for free and open-source contributors working on freedom tech and projects that help bitcoin flourish.",
+ lightningAddress: "opensats@vlt.ge",
+ logo: opensats,
+ },
+ {
+ name: "Brink",
+ description:
+ "Brink exists to strengthen the Bitcoin protocol and network through fundamental research, development, funding, mentoring.",
+ lightningAddress: "bitcoinbrink@zbd.gg",
+ logo: bitcoinbrink,
+ },
+];
+
+export function ZapPlanner() {
+ const { data: apps, mutate: reloadApps } = useApps();
+ const { toast } = useToast();
+
+ const [open, setOpen] = React.useState(false);
+ const [isSubmitting, setSubmitting] = React.useState(false);
+ const [recipientName, setRecipientName] = React.useState("");
+ const [recipientLightningAddress, setRecipientLightningAddress] =
+ React.useState("");
+ const [amount, setAmount] = React.useState("");
+ const [comment, setComment] = React.useState("");
+ const [senderName, setSenderName] = React.useState("");
+
+ React.useEffect(() => {
+ // reset form on close
+ if (!open) {
+ setRecipientName("");
+ setRecipientLightningAddress("");
+ setComment("");
+ setAmount("5000");
+ setSenderName("");
+ }
+ }, [open]);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setSubmitting(true);
+ try {
+ if (apps?.some((existingApp) => existingApp.name === recipientName)) {
+ throw new Error("A connection with the same name already exists.");
+ }
+
+ // validate lighning address
+ const ln = new LightningAddress(recipientLightningAddress);
+ await ln.fetch();
+ if (!ln.lnurlpData) {
+ throw new Error("invalid recipient lightning address");
+ }
+ const parsedAmount = parseInt(amount);
+ if (isNaN(parsedAmount) || parsedAmount < 1) {
+ throw new Error("Invalid amount");
+ }
+
+ const maxAmount = Math.floor(parsedAmount * 1.01) + 10; // with fee reserve
+ const isolated = false;
+
+ const createAppRequest: CreateAppRequest = {
+ name: `ZapPlanner - ${recipientName}`,
+ scopes: ["pay_invoice"],
+ budgetRenewal: "monthly",
+ maxAmount,
+ isolated,
+ metadata: {
+ app_store_app_id: "zapplanner",
+ recipient_lightning_address: recipientLightningAddress,
+ },
+ };
+
+ const createAppResponse = await createApp(createAppRequest);
+
+ // TODO: proxy through hub backend and remove CSRF exceptions for zapplanner.albylabs.com
+ const createSubscriptionResponse = await fetch(
+ "https://zapplanner.albylabs.com/api/subscriptions",
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ recipientLightningAddress: recipientLightningAddress,
+ amount: parsedAmount,
+ message: comment || "ZapPlanner payment from Alby Hub",
+ payerData: JSON.stringify({
+ ...(senderName ? { name: senderName } : {}),
+ }),
+ nostrWalletConnectUrl: createAppResponse.pairingUri,
+ sleepDuration: "31 days",
+ }),
+ }
+ );
+ if (!createSubscriptionResponse.ok) {
+ throw new Error(
+ "Failed to create subscription: " + createSubscriptionResponse.status
+ );
+ }
+
+ const { subscriptionId } = await createSubscriptionResponse.json();
+ if (!subscriptionId) {
+ throw new Error("no subscription ID in create subscription response");
+ }
+
+ // add the ZapPlanner subscription ID to the app metadata
+ const updateAppRequest: UpdateAppRequest = {
+ name: createAppRequest.name,
+ scopes: createAppRequest.scopes,
+ budgetRenewal: createAppRequest.budgetRenewal!,
+ expiresAt: createAppRequest.expiresAt,
+ maxAmount,
+ isolated,
+ metadata: {
+ ...createAppRequest.metadata,
+ zapplanner_subscription_id: subscriptionId,
+ },
+ };
+
+ await request(`/api/apps/${createAppResponse.pairingPublicKey}`, {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(updateAppRequest),
+ });
+
+ toast({
+ title: "Created subscription",
+ description: "The first payment is scheduled immediately.",
+ });
+
+ reloadApps();
+ setOpen(false);
+ } catch (error) {
+ handleRequestError(toast, "Failed to create app", error);
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const zapplannerApps = apps?.filter(
+ (app) => app.metadata?.app_store_app_id === "zapplanner"
+ );
+
+ return (
+
+
+
+
+
+
+ New Recurring Payment
+
+
+
+
+
+
+ >
+ }
+ />
+
+ ZapPlanner is a tool to securely schedule recurring payments. A new
+ special app connection with a strict budget is created for each
+ scheduled payment. This allows you to securely setup recurring payments
+ and be in full control.
+
+
+ {recipients.map((recipient) => (
+
+
+
+
+
+ {recipient.name}
+ {
+ setRecipientName(recipient.name);
+ setRecipientLightningAddress(recipient.lightningAddress);
+ setOpen(true);
+ }}
+ >
+ Support
+
+
+
+ {recipient.description}
+
+
+ ))}
+
+
+ {!!zapplannerApps?.length && (
+ <>
+ Recurring Payments
+
+ {zapplannerApps.map((app, index) => (
+
+ View
+
+ ) : undefined
+ }
+ />
+ ))}
+
+ >
+ )}
+
+ );
+}
diff --git a/frontend/src/screens/settings/AutoUnlock.tsx b/frontend/src/screens/settings/AutoUnlock.tsx
new file mode 100644
index 000000000..6788eb51e
--- /dev/null
+++ b/frontend/src/screens/settings/AutoUnlock.tsx
@@ -0,0 +1,120 @@
+import { AlertTriangle } from "lucide-react";
+import React from "react";
+
+import Container from "src/components/Container";
+import Loading from "src/components/Loading";
+import SettingsHeader from "src/components/SettingsHeader";
+import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert";
+import { Input } from "src/components/ui/input";
+import { Label } from "src/components/ui/label";
+import { LoadingButton } from "src/components/ui/loading-button";
+import { useToast } from "src/components/ui/use-toast";
+
+import { useInfo } from "src/hooks/useInfo";
+import { request } from "src/utils/request";
+
+export function AutoUnlock() {
+ const { toast } = useToast();
+ const { data: info, mutate: refetchInfo } = useInfo();
+
+ const [unlockPassword, setUnlockPassword] = React.useState("");
+ const [loading, setLoading] = React.useState(false);
+
+ const onSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ try {
+ setLoading(true);
+ await request("/api/auto-unlock", {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ unlockPassword,
+ }),
+ });
+ await refetchInfo();
+ setUnlockPassword("");
+ toast({
+ title: `Successfully ${unlockPassword ? "enabled" : "disabled"} auto-unlock`,
+ });
+ } catch (error) {
+ toast({
+ title: "Auto Unlock change failed",
+ description: (error as Error).message,
+ variant: "destructive",
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (!info) {
+ return ;
+ }
+ if (!info.autoUnlockPasswordSupported) {
+ return Your Hub does not support this feature.
;
+ }
+
+ return (
+ <>
+
+
+
+ In some situations it can be impractical to manually unlock the wallet
+ every time Alby Hub is started. In those cases you can save the unlock
+ password in plaintext so that Alby Hub can auto-unlock itself.
+
+
+
+ Attention
+
+ Everyone who has access to the machine running this hub could read
+ that password and take your funds. Use this only in a secure
+ environment.
+
+
+ {!info.autoUnlockPasswordEnabled && (
+ <>
+
+ >
+ )}
+ {info.autoUnlockPasswordEnabled && (
+ <>
+
+ >
+ )}
+
+ >
+ );
+}
diff --git a/frontend/src/screens/wallet/WithdrawOnchainFunds.tsx b/frontend/src/screens/wallet/WithdrawOnchainFunds.tsx
index bdfb38e5e..186104f12 100644
--- a/frontend/src/screens/wallet/WithdrawOnchainFunds.tsx
+++ b/frontend/src/screens/wallet/WithdrawOnchainFunds.tsx
@@ -258,20 +258,22 @@ export default function WithdrawOnchainFunds() {
Confirm Onchain Transaction
-
- Please confirm your payment to{" "}
- {onchainAddress}
-
-
- Amount:{" "}
-
- {sendAll ? (
- "entire on-chain balance"
- ) : (
- <>{new Intl.NumberFormat().format(+amount)} sats>
- )}
-
-
+
+
Please confirm your payment to
+
+ {onchainAddress}
+
+
+ Amount:{" "}
+
+ {sendAll ? (
+ "entire on-chain balance"
+ ) : (
+ <>{new Intl.NumberFormat().format(+amount)} sats>
+ )}
+
+
+
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index dfa94795e..2d62ea535 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -153,6 +153,8 @@ export interface InfoResponse {
enableAdvancedSetup: boolean;
startupError: string;
startupErrorTime: string;
+ autoUnlockPasswordSupported: boolean;
+ autoUnlockPasswordEnabled: boolean;
}
export type Network = "bitcoin" | "testnet" | "signet";
@@ -170,7 +172,7 @@ export interface CreateAppRequest {
name: string;
pubkey?: string;
maxAmount?: number;
- budgetRenewal?: string;
+ budgetRenewal?: BudgetRenewalType;
expiresAt?: string;
scopes: Scope[];
returnTo?: string;
@@ -204,6 +206,7 @@ export type Channel = {
remotePubkey: string;
id: string;
fundingTxId: string;
+ fundingTxVout: number;
active: boolean;
public: boolean;
confirmations?: number;
@@ -276,6 +279,13 @@ export type OnchainBalanceResponse = {
total: number;
reserved: number;
pendingBalancesFromChannelClosures: number;
+ pendingBalancesDetails: {
+ channelId: string;
+ nodeId: string;
+ amount: number;
+ fundingTxId: string;
+ fundingTxVout: number;
+ }[];
};
// from https://mempool.space/docs/api/rest#get-node-stats
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index ce32a7884..471e91359 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -88,7 +88,7 @@ const insertDevCSPPlugin: Plugin = {
"",
`
- `
+ `
);
},
},
diff --git a/go.mod b/go.mod
index 012dc505e..a78ad8cae 100644
--- a/go.mod
+++ b/go.mod
@@ -9,10 +9,10 @@ require (
github.com/breez/breez-sdk-go v0.5.2
github.com/elnosh/gonuts v0.2.0
github.com/getAlby/glalby-go v0.0.0-20240621192717-95673c864d59
- github.com/getAlby/ldk-node-go v0.0.0-20241211081207-8911834564db
+ github.com/getAlby/ldk-node-go v0.0.0-20250106052504-d4191410486f
github.com/go-gormigrate/gormigrate/v2 v2.1.3
- github.com/labstack/echo/v4 v4.12.0
- github.com/nbd-wtf/go-nostr v0.42.3
+ github.com/labstack/echo/v4 v4.13.0
+ github.com/nbd-wtf/go-nostr v0.45.0
github.com/nbd-wtf/ln-decodepay v1.13.0
github.com/orandin/lumberjackrus v1.0.1
github.com/stretchr/testify v1.10.0
@@ -21,7 +21,7 @@ require (
golang.org/x/crypto v0.30.0
golang.org/x/oauth2 v0.24.0
google.golang.org/grpc v1.68.0
- gopkg.in/DataDog/dd-trace-go.v1 v1.69.1
+ gopkg.in/DataDog/dd-trace-go.v1 v1.70.2
gopkg.in/macaroon.v2 v2.1.0
gorm.io/driver/sqlite v1.5.6
gorm.io/gorm v1.25.12
@@ -31,7 +31,7 @@ require (
dario.cat/mergo v1.0.1 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
- github.com/DataDog/datadog-go/v5 v5.3.0 // indirect
+ github.com/DataDog/datadog-go/v5 v5.5.0 // indirect
github.com/DataDog/gostackparse v0.7.0 // indirect
github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect
github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect
@@ -80,7 +80,6 @@ require (
github.com/gobwas/ws v1.4.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
- github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang-migrate/migrate/v4 v4.18.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
@@ -195,6 +194,8 @@ require (
go.etcd.io/etcd/pkg/v3 v3.5.16 // indirect
go.etcd.io/etcd/raft/v3 v3.5.16 // indirect
go.etcd.io/etcd/server/v3 v3.5.16 // indirect
+ go.opentelemetry.io/collector/pdata v1.11.0 // indirect
+ go.opentelemetry.io/collector/pdata/pprofile v0.104.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 // indirect
go.opentelemetry.io/otel v1.30.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 // indirect
@@ -207,12 +208,12 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/mod v0.21.0 // indirect
- golang.org/x/net v0.29.0 // indirect
+ golang.org/x/net v0.31.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect
- golang.org/x/time v0.6.0 // indirect
+ golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.25.0 // indirect
google.golang.org/genproto v0.0.0-20240930140551-af27646dc61f // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f // indirect
@@ -244,12 +245,12 @@ require (
github.com/gorilla/websocket v1.5.3 // indirect
github.com/joho/godotenv v1.5.1
github.com/kelseyhightower/envconfig v1.4.0
- github.com/labstack/echo-jwt/v4 v4.2.0
+ github.com/labstack/echo-jwt/v4 v4.3.0
github.com/lightningnetwork/lnd v0.18.4-beta.rc1
github.com/sirupsen/logrus v1.9.3
github.com/tyler-smith/go-bip32 v1.0.0
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
- gorm.io/datatypes v1.2.4
+ gorm.io/datatypes v1.2.5
)
// See https://github.com/lightningnetwork/lnd/blob/v0.17.4-beta/go.mod#L12C58-L12C70
diff --git a/go.sum b/go.sum
index 82f7f809d..149c058ca 100644
--- a/go.sum
+++ b/go.sum
@@ -7,20 +7,34 @@ filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/DataDog/appsec-internal-go v1.8.0 h1:1Tfn3LEogntRqZtf88twSApOCAAO3V+NILYhuQIo4J4=
-github.com/DataDog/appsec-internal-go v1.8.0/go.mod h1:wW0cRfWBo4C044jHGwYiyh5moQV2x0AhnwqMuiX7O/g=
-github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 h1:bUMSNsw1iofWiju9yc1f+kBd33E3hMJtq9GuU602Iy8=
-github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0/go.mod h1:HzySONXnAgSmIQfL6gOv9hWprKJkx8CicuXuUbmgWfo=
-github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.57.0 h1:LplNAmMgZvGU7kKA0+4c1xWOjz828xweW5TCi8Mw9Q0=
-github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.57.0/go.mod h1:4Vo3SJ24uzfKHUHLoFa8t8o+LH+7TCQ7sPcZDtOpSP4=
-github.com/DataDog/datadog-go/v5 v5.3.0 h1:2q2qjFOb3RwAZNU+ez27ZVDwErJv5/VpbBPprz7Z+s8=
-github.com/DataDog/datadog-go/v5 v5.3.0/go.mod h1:XRDJk1pTc00gm+ZDiBKsjh7oOOtJfYfglVCmFb8C2+Q=
-github.com/DataDog/go-libddwaf/v3 v3.4.0 h1:NJ2W2vhYaOm1OWr1LJCbdgp7ezG/XLJcQKBmjFwhSuM=
-github.com/DataDog/go-libddwaf/v3 v3.4.0/go.mod h1:n98d9nZ1gzenRSk53wz8l6d34ikxS+hs62A31Fqmyi4=
+github.com/DataDog/appsec-internal-go v1.9.0 h1:cGOneFsg0JTRzWl5U2+og5dbtyW3N8XaYwc5nXe39Vw=
+github.com/DataDog/appsec-internal-go v1.9.0/go.mod h1:wW0cRfWBo4C044jHGwYiyh5moQV2x0AhnwqMuiX7O/g=
+github.com/DataDog/datadog-agent/pkg/obfuscate v0.58.0 h1:nOrRNCHyriM/EjptMrttFOQhRSmvfagESdpyknb5VPg=
+github.com/DataDog/datadog-agent/pkg/obfuscate v0.58.0/go.mod h1:MfDvphBMmEMwE3a30h27AtPO7OzmvdoVTiGY1alEmo4=
+github.com/DataDog/datadog-agent/pkg/proto v0.58.0 h1:JX2Q0C5QnKcYqnYHWUcP0z7R0WB8iiQz3aWn+kT5DEc=
+github.com/DataDog/datadog-agent/pkg/proto v0.58.0/go.mod h1:0wLYojGxRZZFQ+SBbFjay9Igg0zbP88l03TfZaVZ6Dc=
+github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.58.0 h1:5hGO0Z8ih0bRojuq+1ZwLFtdgsfO3TqIjbwJAH12sOQ=
+github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.58.0/go.mod h1:jN5BsZI+VilHJV1Wac/efGxS4TPtXa1Lh9SiUyv93F4=
+github.com/DataDog/datadog-agent/pkg/trace v0.58.0 h1:4AjohoBWWN0nNaeD/0SDZ8lRTYmnJ48CqREevUfSets=
+github.com/DataDog/datadog-agent/pkg/trace v0.58.0/go.mod h1:MFnhDW22V5M78MxR7nv7abWaGc/B4L42uHH1KcIKxZs=
+github.com/DataDog/datadog-agent/pkg/util/log v0.58.0 h1:2MENBnHNw2Vx/ebKRyOPMqvzWOUps2Ol2o/j8uMvN4U=
+github.com/DataDog/datadog-agent/pkg/util/log v0.58.0/go.mod h1:1KdlfcwhqtYHS1szAunsgSfvgoiVsf3mAJc+WvNTnIE=
+github.com/DataDog/datadog-agent/pkg/util/scrubber v0.58.0 h1:Jkf91q3tuIer4Hv9CLJIYjlmcelAsoJRMmkHyz+p1Dc=
+github.com/DataDog/datadog-agent/pkg/util/scrubber v0.58.0/go.mod h1:krOxbYZc4KKE7bdEDu10lLSQBjdeSFS/XDSclsaSf1Y=
+github.com/DataDog/datadog-go/v5 v5.5.0 h1:G5KHeB8pWBNXT4Jtw0zAkhdxEAWSpWH00geHI6LDrKU=
+github.com/DataDog/datadog-go/v5 v5.5.0/go.mod h1:K9kcYBlxkcPP8tvvjZZKs/m1edNAUFzBbdpTUKfCsuw=
+github.com/DataDog/go-libddwaf/v3 v3.5.1 h1:GWA4ln4DlLxiXm+X7HA/oj0ZLcdCwOS81KQitegRTyY=
+github.com/DataDog/go-libddwaf/v3 v3.5.1/go.mod h1:n98d9nZ1gzenRSk53wz8l6d34ikxS+hs62A31Fqmyi4=
+github.com/DataDog/go-runtime-metrics-internal v0.0.0-20241106155157-194426bbbd59 h1:s4hgS6gqbXIakEMMujYiHCVVsB3R3oZtqEzPBMnFU2w=
+github.com/DataDog/go-runtime-metrics-internal v0.0.0-20241106155157-194426bbbd59/go.mod h1:quaQJ+wPN41xEC458FCpTwyROZm3MzmTZ8q8XOXQiPs=
+github.com/DataDog/go-sqllexer v0.0.14 h1:xUQh2tLr/95LGxDzLmttLgTo/1gzFeOyuwrQa/Iig4Q=
+github.com/DataDog/go-sqllexer v0.0.14/go.mod h1:KwkYhpFEVIq+BfobkTC1vfqm4gTi65skV/DpDBXtexc=
github.com/DataDog/go-tuf v1.1.0-0.5.2 h1:4CagiIekonLSfL8GMHRHcHudo1fQnxELS9g4tiAupQ4=
github.com/DataDog/go-tuf v1.1.0-0.5.2/go.mod h1:zBcq6f654iVqmkk8n2Cx81E1JnNTMOAx1UEO/wZR+P0=
github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/IGQh4=
github.com/DataDog/gostackparse v0.7.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM=
+github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.20.0 h1:fKv05WFWHCXQmUTehW1eEZvXJP65Qv00W4V01B1EqSA=
+github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.20.0/go.mod h1:dvIWN9pA2zWNTw5rhDWZgzZnhcfpH++d+8d1SWW6xkY=
github.com/DataDog/sketches-go v1.4.5 h1:ki7VfeNz7IcNafq7yI/j5U/YCkO3LJiMDtXz9OMQbyE=
github.com/DataDog/sketches-go v1.4.5/go.mod h1:7Y8GN8Jf66DLyDhc94zuWA3uHEt/7ttt8jHOBWWrSOg=
github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e h1:ahyvB3q25YnZWly5Gq1ekg6jcmWaGj/vG/MhF4aisoc=
@@ -105,6 +119,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 h1:kHaBemcxl8o/pQ5VM1c8PVE1PubbNx3mjUr09OqWGCs=
+github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e h1:0XBUw73chJ1VYSsfvcPvVT7auykAJce9FpRr10L6Qhw=
github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:P13beTBKr5Q18lJe1rIoLUqjM+CB1zYrRg44ZqGuQSA=
@@ -188,8 +204,8 @@ github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwV
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/getAlby/glalby-go v0.0.0-20240621192717-95673c864d59 h1:fSqdXE9uKhLcOOQaLtzN+D8RN3oEcZQkGX5E8PyiKy0=
github.com/getAlby/glalby-go v0.0.0-20240621192717-95673c864d59/go.mod h1:ViyJvjlvv0GCesTJ7mb3fBo4G+/qsujDAFN90xZ7a9U=
-github.com/getAlby/ldk-node-go v0.0.0-20241211081207-8911834564db h1:jHUCoYD74IwOsMOc/99ACajlBNwfTy72AX+e5PoPwwo=
-github.com/getAlby/ldk-node-go v0.0.0-20241211081207-8911834564db/go.mod h1:8BRjtKcz8E0RyYTPEbMS8VIdgredcGSLne8vHDtcRLg=
+github.com/getAlby/ldk-node-go v0.0.0-20250106052504-d4191410486f h1:L9PHEhYgD4tO66KOoTPxYo/+unZC8zTTO5fS32jKG2E=
+github.com/getAlby/ldk-node-go v0.0.0-20250106052504-d4191410486f/go.mod h1:8BRjtKcz8E0RyYTPEbMS8VIdgredcGSLne8vHDtcRLg=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
@@ -227,8 +243,6 @@ github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
-github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
@@ -411,10 +425,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
-github.com/labstack/echo-jwt/v4 v4.2.0 h1:odSISV9JgcSCuhgQSV/6Io3i7nUmfM/QkBeR5GVJj5c=
-github.com/labstack/echo-jwt/v4 v4.2.0/go.mod h1:MA2RqdXdEn4/uEglx0HcUOgQSyBaTh5JcaHIan3biwU=
-github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
-github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
+github.com/labstack/echo-jwt/v4 v4.3.0 h1:8JcvVCrK9dRkPx/aWY3ZempZLO336Bebh4oAtBcxAv4=
+github.com/labstack/echo-jwt/v4 v4.3.0/go.mod h1:OlWm3wqfnq3Ma8DLmmH7GiEAz2S7Bj23im2iPMEAR+Q=
+github.com/labstack/echo/v4 v4.13.0 h1:8DjSi4H/k+RqoOmwXkxW14A2H1pdPdS95+qmdJ4q1Tg=
+github.com/labstack/echo/v4 v4.13.0/go.mod h1:61j7WN2+bp8V21qerqRs4yVlVTGyOagMBpF0vE7VcmM=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
@@ -469,8 +483,8 @@ github.com/ltcsuite/ltcd v0.23.5 h1:MFWjmx2hCwxrUu9v0wdIPOSN7PHg9BWQeh+AO4FsVLI=
github.com/ltcsuite/ltcd v0.23.5/go.mod h1:JV6swXR5m0cYFi0VYdQPp3UnMdaDQxaRUCaU1PPjb+g=
github.com/ltcsuite/ltcd/chaincfg/chainhash v1.0.2 h1:xuWxvRKxLvOKuS7/Q/7I3tpc3cWAB0+hZpU8YdVqkzg=
github.com/ltcsuite/ltcd/chaincfg/chainhash v1.0.2/go.mod h1:nkLkAFGhursWf2U68gt61hPieK1I+0m78e+2aevNyD8=
-github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
-github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
+github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c h1:VtwQ41oftZwlMnOEbMWQtSEUgU64U4s+GHk7hZK+jtY=
+github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
@@ -489,12 +503,12 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
-github.com/microsoft/go-mssqldb v1.0.0 h1:k2p2uuG8T5T/7Hp7/e3vMGTnnR0sU4h8d1CcC71iLHU=
-github.com/microsoft/go-mssqldb v1.0.0/go.mod h1:+4wZTUnz/SV6nffv+RRRB/ss8jPng5Sho2SmM1l2ts4=
+github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
+github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
-github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
-github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE=
+github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
@@ -516,8 +530,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
-github.com/nbd-wtf/go-nostr v0.42.3 h1:wimwmXLhF9ScrNTG4by3eSj2p7HUGkLUospX4bHjxQk=
-github.com/nbd-wtf/go-nostr v0.42.3/go.mod h1:p29g9i1UiSBKdyXkNa6V8rFqE+wrIn4UY0Emabwdu6A=
+github.com/nbd-wtf/go-nostr v0.45.0 h1:4WaMg0Yvda9gBcyRq9KtI32lPeFY8mbX0eFlfdnLrSE=
+github.com/nbd-wtf/go-nostr v0.45.0/go.mod h1:m0ID2gSA2Oak/uaPnM1uN22JhDRZS4UVJG2c8jo19rg=
github.com/nbd-wtf/ln-decodepay v1.13.0 h1:ic32UwT6cBVbLw72fQ7vr0nTMziYTj67baQ2COwlxZk=
github.com/nbd-wtf/ln-decodepay v1.13.0/go.mod h1:SNcdOd7Mv7+PY6Q5E/flUAOfnFdr/W/PK2O6wyzpra8=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
@@ -564,8 +578,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
-github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
+github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI=
+github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI=
github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -600,8 +614,8 @@ github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXn
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/secure-systems-lab/go-securesystemslib v0.7.0 h1:OwvJ5jQf9LnIAS83waAjPbcMsODrTQUpJ02eNLUoxBg=
github.com/secure-systems-lab/go-securesystemslib v0.7.0/go.mod h1:/2gYnlnHVQ6xeGtfIqFy7Do03K4cdCY0A/GlJLDKLHI=
-github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
-github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
+github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU=
+github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
@@ -690,8 +704,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
-github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
+github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
@@ -709,6 +723,16 @@ go.etcd.io/etcd/raft/v3 v3.5.16 h1:zBXA3ZUpYs1AwiLGPafYAKKl/CORn/uaxYDwlNwndAk=
go.etcd.io/etcd/raft/v3 v3.5.16/go.mod h1:P4UP14AxofMJ/54boWilabqqWoW9eLodl6I5GdGzazI=
go.etcd.io/etcd/server/v3 v3.5.16 h1:d0/SAdJ3vVsZvF8IFVb1k8zqMZ+heGcNfft71ul9GWE=
go.etcd.io/etcd/server/v3 v3.5.16/go.mod h1:ynhyZZpdDp1Gq49jkUg5mfkDWZwXnn3eIqCqtJnrD/s=
+go.opentelemetry.io/collector/component v0.104.0 h1:jqu/X9rnv8ha0RNZ1a9+x7OU49KwSMsPbOuIEykHuQE=
+go.opentelemetry.io/collector/component v0.104.0/go.mod h1:1C7C0hMVSbXyY1ycCmaMUAR9fVwpgyiNQqxXtEWhVpw=
+go.opentelemetry.io/collector/config/configtelemetry v0.104.0 h1:eHv98XIhapZA8MgTiipvi+FDOXoFhCYOwyKReOt+E4E=
+go.opentelemetry.io/collector/config/configtelemetry v0.104.0/go.mod h1:WxWKNVAQJg/Io1nA3xLgn/DWLE/W1QOB2+/Js3ACi40=
+go.opentelemetry.io/collector/pdata v1.11.0 h1:rzYyV1zfTQQz1DI9hCiaKyyaczqawN75XO9mdXmR/hE=
+go.opentelemetry.io/collector/pdata v1.11.0/go.mod h1:IHxHsp+Jq/xfjORQMDJjSH6jvedOSTOyu3nbxqhWSYE=
+go.opentelemetry.io/collector/pdata/pprofile v0.104.0 h1:MYOIHvPlKEJbWLiBKFQWGD0xd2u22xGVLt4jPbdxP4Y=
+go.opentelemetry.io/collector/pdata/pprofile v0.104.0/go.mod h1:7WpyHk2wJZRx70CGkBio8klrYTTXASbyIhf+rH4FKnA=
+go.opentelemetry.io/collector/semconv v0.104.0 h1:dUvajnh+AYJLEW/XOPk0T0BlwltSdi3vrjO7nSOos3k=
+go.opentelemetry.io/collector/semconv v0.104.0/go.mod h1:yMVUCNoQPZVq/IPfrHrnntZTWsLf5YGZ7qwKulIl5hw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 h1:hCq2hNMwsegUvPzI7sPOvtO9cqyy5GbWt/Ybp2xrx8Q=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0/go.mod h1:LqaApwGx/oUmzsbqxkzuBvyoPpkxk3JQWnqfVrJ3wCA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
@@ -812,8 +836,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
-golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
-golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
+golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
+golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
@@ -892,8 +916,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
-golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
-golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
+golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181008205924-a2b3f7f249e9/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -945,8 +969,8 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
-gopkg.in/DataDog/dd-trace-go.v1 v1.69.1 h1:grTElrPaCfxUsrJjyPLHlVPbmlKVzWMxVdcBrGZSzEk=
-gopkg.in/DataDog/dd-trace-go.v1 v1.69.1/go.mod h1:U9AOeBHNAL95JXcd/SPf4a7O5GNeF/yD13sJtli/yaU=
+gopkg.in/DataDog/dd-trace-go.v1 v1.70.2 h1:MVckfRl7BcC9cf5X35NK3c4Uop5BKZrfAjKWutqtXdk=
+gopkg.in/DataDog/dd-trace-go.v1 v1.70.2/go.mod h1:CVUgctrrPGeB+OSjgyt56CNH5QxQwW3t11QU8R1LQjQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@@ -958,6 +982,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/httprequest.v1 v1.2.0/go.mod h1:T61ZUaJLpMnzvoJDO03ZD8yRXD4nZzBeDoW5e9sffjg=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/juju/environschema.v1 v1.0.0/go.mod h1:WTgU3KXKCVoO9bMmG/4KHzoaRvLeoxfjArpgd1MGWFA=
gopkg.in/macaroon-bakery.v2 v2.3.0 h1:b40knPgPTke1QLTE8BSYeH7+R/hiIozB1A8CTLYN0Ic=
gopkg.in/macaroon-bakery.v2 v2.3.0/go.mod h1:/8YhtPARXeRzbpEPLmRB66+gQE8/pzBBkWwg7Vz/guc=
@@ -980,16 +1006,16 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gorm.io/datatypes v1.2.4 h1:uZmGAcK/QZ0uyfCuVg0VQY1ZmV9h1fuG0tMwKByO1z4=
-gorm.io/datatypes v1.2.4/go.mod h1:f4BsLcFAX67szSv8svwLRjklArSHAvHLeE3pXAS5DZI=
+gorm.io/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I=
+gorm.io/datatypes v1.2.5/go.mod h1:I5FUdlKpLb5PMqeMQhm30CQ6jXP8Rj89xkTeCSAaAD4=
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U=
gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A=
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
-gorm.io/driver/sqlserver v1.4.2 h1:nMtEeKqv2R/vv9FoHUFWfXfP6SskAgRar0TPlZV1stk=
-gorm.io/driver/sqlserver v1.4.2/go.mod h1:XHwBuB4Tlh7DqO0x7Ema8dmyWsQW7wi38VQOAFkrbXY=
+gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g=
+gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
diff --git a/http/http_service.go b/http/http_service.go
index 12f4982b9..4fc718fd8 100644
--- a/http/http_service.go
+++ b/http/http_service.go
@@ -63,7 +63,7 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) {
e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
ContentTypeNosniff: "nosniff",
XFrameOptions: "DENY",
- ContentSecurityPolicy: "default-src 'self'; img-src 'self' https://uploads.getalby-assets.com https://getalby.com; connect-src 'self' https://api.getalby.com https://getalby.com",
+ ContentSecurityPolicy: "default-src 'self'; img-src 'self' https://uploads.getalby-assets.com https://getalby.com; connect-src 'self' https://api.getalby.com https://getalby.com https://zapplanner.albylabs.com",
ReferrerPolicy: "no-referrer",
}))
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
@@ -98,6 +98,7 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) {
e.POST("/api/start", httpSvc.startHandler, unlockRateLimiter)
e.POST("/api/unlock", httpSvc.unlockHandler, unlockRateLimiter)
e.PATCH("/api/unlock-password", httpSvc.changeUnlockPasswordHandler, unlockRateLimiter)
+ e.PATCH("/api/auto-unlock", httpSvc.autoUnlockHandler, unlockRateLimiter)
e.POST("/api/backup", httpSvc.createBackupHandler, unlockRateLimiter)
e.GET("/logout", httpSvc.logoutHandler, unlockRateLimiter)
@@ -291,6 +292,24 @@ func (httpSvc *HttpService) changeUnlockPasswordHandler(c echo.Context) error {
return c.NoContent(http.StatusNoContent)
}
+func (httpSvc *HttpService) autoUnlockHandler(c echo.Context) error {
+ var autoUnlockRequest api.AutoUnlockRequest
+ if err := c.Bind(&autoUnlockRequest); err != nil {
+ return c.JSON(http.StatusBadRequest, ErrorResponse{
+ Message: fmt.Sprintf("Bad request: %s", err.Error()),
+ })
+ }
+
+ err := httpSvc.api.SetAutoUnlockPassword(autoUnlockRequest.UnlockPassword)
+ if err != nil {
+ return c.JSON(http.StatusInternalServerError, ErrorResponse{
+ Message: fmt.Sprintf("Failed to set auto unlock password: %s", err.Error()),
+ })
+ }
+
+ return c.NoContent(http.StatusNoContent)
+}
+
func (httpSvc *HttpService) createJWT(tokenExpiryDays *uint64) (string, error) {
expiryDays := uint64(30)
if tokenExpiryDays != nil {
diff --git a/lnclient/ldk/ldk.go b/lnclient/ldk/ldk.go
index 7b23981a5..e12d2f335 100644
--- a/lnclient/ldk/ldk.go
+++ b/lnclient/ldk/ldk.go
@@ -3,7 +3,8 @@ package ldk
import (
"context"
"crypto/sha256"
- "database/sql"
+ "encoding/hex"
+ "encoding/json"
"errors"
"fmt"
"math"
@@ -20,9 +21,6 @@ import (
// "github.com/getAlby/hub/ldk_node"
- "encoding/hex"
- "encoding/json"
-
decodepay "github.com/nbd-wtf/ln-decodepay"
"github.com/sirupsen/logrus"
@@ -32,6 +30,7 @@ import (
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/lsp"
"github.com/getAlby/hub/service/keys"
+ "github.com/getAlby/hub/transactions"
"github.com/getAlby/hub/utils"
)
@@ -57,7 +56,7 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events
return nil, errors.New("one or more required LDK configuration are missing")
}
- //create dir if not exists
+ // create dir if not exists
newpath := filepath.Join(workDir)
err = os.MkdirAll(newpath, os.ModePerm)
if err != nil {
@@ -105,6 +104,7 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events
// If LogLevelGossip is changed to 0, this addition can be removed
ldkConfig.LogLevel = ldk_node.LogLevel(logLevel) + ldk_node.LogLevelGossip
}
+ ldkConfig.TransientNetworkGraph = cfg.GetEnv().LDKTransientNetworkGraph
builder := ldk_node.BuilderFromConfig(ldkConfig)
builder.SetNodeAlias("Alby Hub") // TODO: allow users to customize
builder.SetEntropyBip39Mnemonic(mnemonic, nil)
@@ -133,6 +133,11 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events
builder.MigrateStorage(ldk_node.MigrateStorageVss)
}
+ resetStateRequest := getResetStateRequest(cfg)
+ if resetStateRequest != nil {
+ builder.ResetState(*resetStateRequest)
+ }
+
logger.Logger.WithFields(logrus.Fields{
"migrate_storage": migrateStorage,
"vss_enabled": vssToken != "",
@@ -249,9 +254,6 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events
"duration": math.Ceil(time.Since(syncStartTime).Seconds()),
}).Info("LDK node synced successfully")
- // backup channels after successful startup
- ls.backupChannels()
-
if ls.network == "bitcoin" {
// try to connect to some peers to retrieve P2P gossip data. TODO: Remove once LDK can correctly do gossip with CLN and Eclair nodes
// see https://github.com/lightningdevkit/rust-lightning/issues/3075
@@ -408,67 +410,14 @@ func (ls *LDKService) Shutdown() error {
logger.Logger.Debug("Destroying LDK node object")
node.Destroy()
- ls.resetRouterInternal()
-
logger.Logger.Info("LDK shutdown complete")
return nil
}
-func (ls *LDKService) resetRouterInternal() {
- key, err := ls.cfg.Get(resetRouterKey, "")
-
- if err != nil {
- logger.Logger.Error("Failed to retrieve ResetRouter key")
- return
- }
-
- if key != "" {
- err = ls.cfg.SetUpdate(resetRouterKey, "", "")
- if err != nil {
- logger.Logger.WithError(err).Error("Failed to remove reset router key")
- return
- }
- 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) {
- logger.Logger.Error("Could not find LDK database")
- return
- }
- ldkDb, err := sql.Open("sqlite", ldkDbPath)
- if err != nil {
- logger.Logger.Error("Could not open LDK DB file")
- return
- }
-
- command := ""
-
- switch key {
- case "ALL":
- command = "delete from ldk_node_data where key = 'scorer' or key = 'network_graph';VACUUM;"
- case "Scorer":
- command = "delete from ldk_node_data where key = 'scorer';VACUUM;"
- case "NetworkGraph":
- command = "delete from ldk_node_data where key = 'network_graph';VACUUM;"
- default:
- logger.Logger.WithField("key", key).Error("Unknown reset router key")
- return
- }
-
- result, err := ldkDb.Exec(command)
- if err != nil {
- logger.Logger.WithError(err).Error("Failed execute reset command")
- return
- }
- rowsAffected, err := result.RowsAffected()
- if err != nil {
- logger.Logger.WithError(err).Error("Failed to get rows affected")
- return
- }
- logger.Logger.WithFields(logrus.Fields{
- "rowsAffected": rowsAffected,
- }).Info("Reset router")
+func getMaxTotalRoutingFeeLimit(amountMsat uint64) ldk_node.MaxTotalRoutingFeeLimit {
+ return ldk_node.MaxTotalRoutingFeeLimitSome{
+ AmountMsat: transactions.CalculateFeeReserveMsat(amountMsat),
}
}
@@ -482,19 +431,19 @@ func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string, amoun
return nil, err
}
- paymentAmount := uint64(paymentRequest.MSatoshi)
+ paymentAmountMsat := uint64(paymentRequest.MSatoshi)
if amount != nil {
- paymentAmount = *amount
+ paymentAmountMsat = *amount
}
maxSpendable := ls.getMaxSpendable()
- if paymentAmount > maxSpendable {
+ if paymentAmountMsat > maxSpendable {
ls.eventPublisher.Publish(&events.Event{
Event: "nwc_outgoing_liquidity_required",
Properties: map[string]interface{}{
- //"amount": amount / 1000,
- //"max_receivable": maxReceivable,
- //"num_channels": len(gs.node.ListChannels()),
+ // "amount": amount / 1000,
+ // "max_receivable": maxReceivable,
+ // "num_channels": len(gs.node.ListChannels()),
"node_type": config.LDKBackendType,
},
})
@@ -505,10 +454,15 @@ func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string, amoun
defer ls.ldkEventBroadcaster.CancelSubscription(ldkEventSubscription)
var paymentHash string
+ maxTotalRoutingFeeMsat := getMaxTotalRoutingFeeLimit(paymentAmountMsat)
+ sendingParams := &ldk_node.SendingParameters{
+ MaxTotalRoutingFeeMsat: &maxTotalRoutingFeeMsat,
+ }
+
if amount == nil {
- paymentHash, err = ls.node.Bolt11Payment().Send(invoice, nil)
+ paymentHash, err = ls.node.Bolt11Payment().Send(invoice, sendingParams)
} else {
- paymentHash, err = ls.node.Bolt11Payment().SendUsingAmount(invoice, *amount, nil)
+ paymentHash, err = ls.node.Bolt11Payment().SendUsingAmount(invoice, *amount, sendingParams)
}
if err != nil {
logger.Logger.WithError(err).Error("SendPayment failed")
@@ -517,7 +471,7 @@ func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string, amoun
fee := uint64(0)
preimage := ""
- for start := time.Now(); time.Since(start) < time.Second*60; {
+ for start := time.Now(); time.Since(start) < time.Second*50; {
event := <-ldkEventSubscription
eventPaymentSuccessful, isEventPaymentSuccessfulEvent := (*event).(ldk_node.EventPaymentSuccessful)
@@ -597,14 +551,19 @@ func (ls *LDKService) SendKeysend(ctx context.Context, amount uint64, destinatio
ldkEventSubscription := ls.ldkEventBroadcaster.Subscribe()
defer ls.ldkEventBroadcaster.CancelSubscription(ldkEventSubscription)
- paymentHash, err := ls.node.SpontaneousPayment().Send(amount, destination, nil, customTlvs, &preimage)
+ maxTotalRoutingFeeMsat := getMaxTotalRoutingFeeLimit(amount)
+ sendingParams := &ldk_node.SendingParameters{
+ MaxTotalRoutingFeeMsat: &maxTotalRoutingFeeMsat,
+ }
+
+ paymentHash, err := ls.node.SpontaneousPayment().Send(amount, destination, sendingParams, customTlvs, &preimage)
if err != nil {
logger.Logger.WithError(err).Error("Keysend failed")
return nil, err
}
fee := uint64(0)
paid := false
- for start := time.Now(); time.Since(start) < time.Second*60; {
+ for start := time.Now(); time.Since(start) < time.Second*50; {
event := <-ldkEventSubscription
eventPaymentSuccessful, isEventPaymentSuccessfulEvent := (*event).(ldk_node.EventPaymentSuccessful)
@@ -678,9 +637,9 @@ func (ls *LDKService) MakeInvoice(ctx context.Context, amount int64, description
ls.eventPublisher.Publish(&events.Event{
Event: "nwc_incoming_liquidity_required",
Properties: map[string]interface{}{
- //"amount": amount / 1000,
- //"max_receivable": maxReceivable,
- //"num_channels": len(gs.node.ListChannels()),
+ // "amount": amount / 1000,
+ // "max_receivable": maxReceivable,
+ // "num_channels": len(gs.node.ListChannels()),
"node_type": config.LDKBackendType,
},
})
@@ -827,8 +786,10 @@ func (ls *LDKService) ListChannels(ctx context.Context) ([]lnclient.Channel, err
for _, ldkChannel := range ldkChannels {
fundingTxId := ""
+ fundingTxVout := uint32(0)
if ldkChannel.FundingTxo != nil {
fundingTxId = ldkChannel.FundingTxo.Txid
+ fundingTxVout = ldkChannel.FundingTxo.Vout
}
internalChannel := map[string]interface{}{}
@@ -870,6 +831,7 @@ func (ls *LDKService) ListChannels(ctx context.Context) ([]lnclient.Channel, err
Active: isActive,
Public: ldkChannel.IsAnnounced,
FundingTxId: fundingTxId,
+ FundingTxVout: fundingTxVout,
Confirmations: ldkChannel.Confirmations,
ConfirmationsRequired: ldkChannel.ConfirmationsRequired,
ForwardingFeeBaseMsat: ldkChannel.Config.ForwardingFeeBaseMsat,
@@ -901,8 +863,8 @@ func (ls *LDKService) GetNodeConnectionInfo(ctx context.Context) (nodeConnection
return &lnclient.NodeConnectionInfo{
Pubkey: ls.node.NodeId(),
- //Address: parts[0],
- //Port: port,
+ // Address: parts[0],
+ // Port: port,
}, nil
}
@@ -1055,15 +1017,24 @@ func (ls *LDKService) GetOnchainBalance(ctx context.Context) (*lnclient.OnchainB
internalLightningBalances := []internalLightningBalance{}
+ pendingBalancesDetails := make([]lnclient.PendingBalanceDetails, 0)
+
pendingBalancesFromChannelClosures := uint64(0)
// increase pending balance from any lightning balances for channels that are pending closure
// (they do not exist in our list of open channels)
for _, balance := range balances.LightningBalances {
- increasePendingBalance := func(channelId string, amount uint64) {
+ increasePendingBalance := func(nodeId, channelId string, amount uint64, fundingTxId ldk_node.Txid, fundingTxIndex uint16) {
if !slices.ContainsFunc(channels, func(channel ldk_node.ChannelDetails) bool {
return channel.ChannelId == channelId
}) {
pendingBalancesFromChannelClosures += amount
+ pendingBalancesDetails = append(pendingBalancesDetails, lnclient.PendingBalanceDetails{
+ NodeId: nodeId,
+ ChannelId: channelId,
+ Amount: amount,
+ FundingTxId: fundingTxId,
+ FundingTxVout: uint32(fundingTxIndex),
+ })
}
}
@@ -1074,17 +1045,17 @@ func (ls *LDKService) GetOnchainBalance(ctx context.Context) (*lnclient.OnchainB
})
switch balanceType := (balance).(type) {
case ldk_node.LightningBalanceClaimableOnChannelClose:
- increasePendingBalance(balanceType.ChannelId, balanceType.AmountSatoshis)
+ increasePendingBalance(balanceType.CounterpartyNodeId, balanceType.ChannelId, balanceType.AmountSatoshis, balanceType.FundingTxId, balanceType.FundingTxIndex)
case ldk_node.LightningBalanceClaimableAwaitingConfirmations:
- increasePendingBalance(balanceType.ChannelId, balanceType.AmountSatoshis)
+ increasePendingBalance(balanceType.CounterpartyNodeId, balanceType.ChannelId, balanceType.AmountSatoshis, balanceType.FundingTxId, balanceType.FundingTxIndex)
case ldk_node.LightningBalanceContentiousClaimable:
- increasePendingBalance(balanceType.ChannelId, balanceType.AmountSatoshis)
+ increasePendingBalance(balanceType.CounterpartyNodeId, balanceType.ChannelId, balanceType.AmountSatoshis, balanceType.FundingTxId, balanceType.FundingTxIndex)
case ldk_node.LightningBalanceMaybeTimeoutClaimableHtlc:
- increasePendingBalance(balanceType.ChannelId, balanceType.AmountSatoshis)
+ increasePendingBalance(balanceType.CounterpartyNodeId, balanceType.ChannelId, balanceType.AmountSatoshis, balanceType.FundingTxId, balanceType.FundingTxIndex)
case ldk_node.LightningBalanceMaybePreimageClaimableHtlc:
- increasePendingBalance(balanceType.ChannelId, balanceType.AmountSatoshis)
+ increasePendingBalance(balanceType.CounterpartyNodeId, balanceType.ChannelId, balanceType.AmountSatoshis, balanceType.FundingTxId, balanceType.FundingTxIndex)
case ldk_node.LightningBalanceCounterpartyRevokedOutputClaimable:
- increasePendingBalance(balanceType.ChannelId, balanceType.AmountSatoshis)
+ increasePendingBalance(balanceType.CounterpartyNodeId, balanceType.ChannelId, balanceType.AmountSatoshis, balanceType.FundingTxId, balanceType.FundingTxIndex)
}
}
@@ -1105,6 +1076,7 @@ func (ls *LDKService) GetOnchainBalance(ctx context.Context) (*lnclient.OnchainB
Total: int64(balances.TotalOnchainBalanceSats - balances.TotalAnchorChannelsReserveSats),
Reserved: int64(balances.TotalAnchorChannelsReserveSats),
PendingBalancesFromChannelClosures: pendingBalancesFromChannelClosures,
+ PendingBalancesDetails: pendingBalancesDetails,
InternalBalances: map[string]interface{}{
"internal_lightning_balances": internalLightningBalances,
"all_balances": balances,
@@ -1802,3 +1774,37 @@ func GetVssNodeIdentifier(keys keys.Keys) (string, error) {
pubkeyHashBytes := pubkeyHash256.Sum(nil)
return hex.EncodeToString(pubkeyHashBytes[0:3]), nil
}
+
+func getResetStateRequest(cfg config.Config) *ldk_node.ResetState {
+ resetKey, err := cfg.Get(resetRouterKey, "")
+ if err != nil {
+ logger.Logger.Error("Failed to retrieve ResetRouter key")
+ return nil
+ }
+
+ if resetKey == "" {
+ return nil
+ }
+
+ err = cfg.SetUpdate(resetRouterKey, "", "")
+ if err != nil {
+ logger.Logger.WithError(err).Error("Failed to remove reset router key")
+ return nil
+ }
+
+ var ret ldk_node.ResetState
+
+ switch resetKey {
+ case "ALL":
+ ret = ldk_node.ResetStateAll
+ case "Scorer":
+ ret = ldk_node.ResetStateScorer
+ case "NetworkGraph":
+ ret = ldk_node.ResetStateNetworkGraph
+ default:
+ logger.Logger.WithField("key", resetKey).Error("Unknown reset router key")
+ return nil
+ }
+
+ return &ret
+}
diff --git a/lnclient/lnd/lnd.go b/lnclient/lnd/lnd.go
index e0f8d1119..46d9bd472 100644
--- a/lnclient/lnd/lnd.go
+++ b/lnclient/lnd/lnd.go
@@ -23,6 +23,7 @@ import (
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/lnclient/lnd/wrapper"
"github.com/getAlby/hub/logger"
+ "github.com/getAlby/hub/transactions"
"github.com/sirupsen/logrus"
// "gorm.io/gorm"
@@ -198,6 +199,7 @@ func (svc *LNDService) ListChannels(ctx context.Context) ([]lnclient.Channel, er
Active: lndChannel.Active,
Public: !lndChannel.Private,
FundingTxId: channelPoint.GetFundingTxidStr(),
+ FundingTxVout: channelPoint.GetOutputIndex(),
Confirmations: &confirmations,
ConfirmationsRequired: &confirmationsRequired,
UnspendablePunishmentReserve: lndChannel.LocalConstraints.ChanReserveSat,
@@ -313,34 +315,67 @@ func (svc *LNDService) LookupInvoice(ctx context.Context, paymentHash string) (t
return transaction, nil
}
+func (svc *LNDService) getPaymentResult(stream routerrpc.Router_SendPaymentV2Client) (*lnrpc.Payment, error) {
+ for {
+ payment, err := stream.Recv()
+ if err != nil {
+ return nil, err
+ }
+
+ if payment.Status != lnrpc.Payment_IN_FLIGHT {
+ return payment, nil
+ }
+ }
+}
+
func (svc *LNDService) SendPaymentSync(ctx context.Context, payReq string, amount *uint64) (*lnclient.PayInvoiceResponse, error) {
- sendRequest := &lnrpc.SendRequest{PaymentRequest: payReq}
+ const MAX_PARTIAL_PAYMENTS = 16
+ const SEND_PAYMENT_TIMEOUT = 50
+ paymentRequest, err := decodepay.Decodepay(payReq)
+ if err != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "bolt11": payReq,
+ }).WithError(err).Error("Failed to decode bolt11 invoice")
+
+ return nil, err
+ }
+
+ paymentAmountMsat := uint64(paymentRequest.MSatoshi)
+ if amount != nil {
+ paymentAmountMsat = *amount
+ }
+ sendRequest := &routerrpc.SendPaymentRequest{
+ PaymentRequest: payReq,
+ MaxParts: MAX_PARTIAL_PAYMENTS,
+ TimeoutSeconds: SEND_PAYMENT_TIMEOUT,
+ FeeLimitMsat: int64(transactions.CalculateFeeReserveMsat(paymentAmountMsat)),
+ }
if amount != nil {
sendRequest.AmtMsat = int64(*amount)
}
- resp, err := svc.client.SendPaymentSync(ctx, sendRequest)
+ payStream, err := svc.client.SendPayment(ctx, sendRequest)
if err != nil {
return nil, err
}
- if resp.PaymentError != "" {
- return nil, errors.New(resp.PaymentError)
+ resp, err := svc.getPaymentResult(payStream)
+ if err != nil {
+ return nil, err
}
- if resp.PaymentPreimage == nil {
- return nil, errors.New("no preimage in response")
+ if resp.Status != lnrpc.Payment_SUCCEEDED {
+ return nil, errors.New(resp.FailureReason.String())
}
- var fee uint64 = 0
- if resp.PaymentRoute != nil {
- fee = uint64(resp.PaymentRoute.TotalFeesMsat)
+ if resp.PaymentPreimage == "" {
+ return nil, errors.New("no preimage in response")
}
return &lnclient.PayInvoiceResponse{
- Preimage: hex.EncodeToString(resp.PaymentPreimage),
- Fee: fee,
+ Preimage: resp.PaymentPreimage,
+ Fee: uint64(resp.FeeMsat),
}, nil
}
@@ -370,17 +405,22 @@ func (svc *LNDService) SendKeysend(ctx context.Context, amount uint64, destinati
}
destCustomRecords[record.Type] = decodedValue
}
+ const MAX_PARTIAL_PAYMENTS = 16
+ const SEND_PAYMENT_TIMEOUT = 50
const KEYSEND_CUSTOM_RECORD = 5482373484
destCustomRecords[KEYSEND_CUSTOM_RECORD] = preImageBytes
- sendPaymentRequest := &lnrpc.SendRequest{
+ sendPaymentRequest := &routerrpc.SendPaymentRequest{
Dest: destBytes,
AmtMsat: int64(amount),
PaymentHash: paymentHashBytes,
DestFeatures: []lnrpc.FeatureBit{lnrpc.FeatureBit_TLV_ONION_REQ},
DestCustomRecords: destCustomRecords,
+ MaxParts: MAX_PARTIAL_PAYMENTS,
+ TimeoutSeconds: SEND_PAYMENT_TIMEOUT,
+ FeeLimitMsat: int64(transactions.CalculateFeeReserveMsat(amount)),
}
- resp, err := svc.client.SendPaymentSync(ctx, sendPaymentRequest)
+ payStream, err := svc.client.SendPayment(ctx, sendPaymentRequest)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"amount": amount,
@@ -392,26 +432,31 @@ func (svc *LNDService) SendKeysend(ctx context.Context, amount uint64, destinati
}).Errorf("Failed to send keysend payment")
return nil, err
}
- if resp.PaymentError != "" {
+
+ resp, err := svc.getPaymentResult(payStream)
+ if err != nil {
+ return nil, err
+ }
+
+ if resp.Status != lnrpc.Payment_SUCCEEDED {
logger.Logger.WithFields(logrus.Fields{
"amount": amount,
"payeePubkey": destination,
"paymentHash": paymentHash,
"preimage": preimage,
"customRecords": custom_records,
- "paymentError": resp.PaymentError,
+ "paymentError": resp.FailureReason.String(),
}).Errorf("Keysend payment has payment error")
- return nil, errors.New(resp.PaymentError)
+ return nil, errors.New(resp.FailureReason.String())
}
- respPreimage := hex.EncodeToString(resp.PaymentPreimage)
- if respPreimage != preimage {
+
+ if resp.PaymentPreimage != preimage {
logger.Logger.WithFields(logrus.Fields{
"amount": amount,
"payeePubkey": destination,
"paymentHash": paymentHash,
"preimage": preimage,
"customRecords": custom_records,
- "paymentError": resp.PaymentError,
}).Errorf("Preimage in keysend response does not match")
return nil, errors.New("preimage in keysend response does not match")
}
@@ -421,11 +466,11 @@ func (svc *LNDService) SendKeysend(ctx context.Context, amount uint64, destinati
"paymentHash": paymentHash,
"preimage": preimage,
"customRecords": custom_records,
- "respPreimage": respPreimage,
+ "respPreimage": resp.PaymentPreimage,
}).Info("Keysend payment successful")
return &lnclient.PayKeysendResponse{
- Fee: uint64(resp.PaymentRoute.TotalFeesMsat),
+ Fee: uint64(resp.FeeMsat),
}, nil
}
@@ -836,8 +881,21 @@ func (svc *LNDService) GetOnchainBalance(ctx context.Context) (*lnclient.Onchain
return nil, err
}
pendingBalancesFromChannelClosures := uint64(0)
+ pendingBalancesDetails := []lnclient.PendingBalanceDetails{}
for _, closingChannel := range pendingChannels.WaitingCloseChannels {
pendingBalancesFromChannelClosures += uint64(closingChannel.LimboBalance)
+ if closingChannel.Channel != nil {
+ channelPoint, err := svc.parseChannelPoint(closingChannel.Channel.ChannelPoint)
+ if err != nil {
+ return nil, err
+ }
+ pendingBalancesDetails = append(pendingBalancesDetails, lnclient.PendingBalanceDetails{
+ NodeId: closingChannel.Channel.RemoteNodePub,
+ Amount: uint64(closingChannel.LimboBalance),
+ FundingTxId: channelPoint.GetFundingTxidStr(),
+ FundingTxVout: channelPoint.GetOutputIndex(),
+ })
+ }
}
logger.Logger.WithFields(logrus.Fields{
"balances": balances,
@@ -847,6 +905,7 @@ func (svc *LNDService) GetOnchainBalance(ctx context.Context) (*lnclient.Onchain
Total: int64(balances.TotalBalance),
Reserved: int64(balances.ReservedBalanceAnchorChan),
PendingBalancesFromChannelClosures: pendingBalancesFromChannelClosures,
+ PendingBalancesDetails: pendingBalancesDetails,
InternalBalances: map[string]interface{}{
"balances": balances,
"pending_channels": pendingChannels,
diff --git a/lnclient/lnd/wrapper/lnd.go b/lnclient/lnd/wrapper/lnd.go
index 92ebac25e..1d86b5d78 100644
--- a/lnclient/lnd/wrapper/lnd.go
+++ b/lnclient/lnd/wrapper/lnd.go
@@ -103,8 +103,8 @@ func (wrapper *LNDWrapper) PendingChannels(ctx context.Context, req *lnrpc.Pendi
return wrapper.client.PendingChannels(ctx, req, options...)
}
-func (wrapper *LNDWrapper) SendPaymentSync(ctx context.Context, req *lnrpc.SendRequest, options ...grpc.CallOption) (*lnrpc.SendResponse, error) {
- return wrapper.client.SendPaymentSync(ctx, req, options...)
+func (wrapper *LNDWrapper) SendPayment(ctx context.Context, req *routerrpc.SendPaymentRequest, options ...grpc.CallOption) (routerrpc.Router_SendPaymentV2Client, error) {
+ return wrapper.routerClient.SendPaymentV2(ctx, req, options...)
}
func (wrapper *LNDWrapper) ChannelBalance(ctx context.Context, req *lnrpc.ChannelBalanceRequest, options ...grpc.CallOption) (*lnrpc.ChannelBalanceResponse, error) {
diff --git a/lnclient/models.go b/lnclient/models.go
index ec99fd566..c5098866f 100644
--- a/lnclient/models.go
+++ b/lnclient/models.go
@@ -86,6 +86,7 @@ type Channel struct {
Id string
RemotePubkey string
FundingTxId string
+ FundingTxVout uint32
Active bool
Public bool
InternalChannel interface{}
@@ -134,12 +135,21 @@ type UpdateChannelRequest struct {
type CloseChannelResponse struct {
}
+type PendingBalanceDetails struct {
+ ChannelId string `json:"channelId"`
+ NodeId string `json:"nodeId"`
+ Amount uint64 `json:"amount"`
+ FundingTxId string `json:"fundingTxId"`
+ FundingTxVout uint32 `json:"fundingTxVout"`
+}
+
type OnchainBalanceResponse struct {
- Spendable int64 `json:"spendable"`
- Total int64 `json:"total"`
- Reserved int64 `json:"reserved"`
- PendingBalancesFromChannelClosures uint64 `json:"pendingBalancesFromChannelClosures"`
- InternalBalances interface{} `json:"internalBalances"`
+ Spendable int64 `json:"spendable"`
+ Total int64 `json:"total"`
+ Reserved int64 `json:"reserved"`
+ PendingBalancesFromChannelClosures uint64 `json:"pendingBalancesFromChannelClosures"`
+ PendingBalancesDetails []PendingBalanceDetails `json:"pendingBalancesDetails"`
+ InternalBalances interface{} `json:"internalBalances"`
}
type PeerDetails struct {
diff --git a/nip47/cipher/cipher.go b/nip47/cipher/cipher.go
new file mode 100644
index 000000000..e1046b81d
--- /dev/null
+++ b/nip47/cipher/cipher.go
@@ -0,0 +1,87 @@
+package cipher
+
+import (
+ "fmt"
+
+ "github.com/nbd-wtf/go-nostr/nip04"
+ "github.com/nbd-wtf/go-nostr/nip44"
+)
+
+const (
+ SUPPORTED_VERSIONS = "1.0 0.0"
+)
+
+type Nip47Cipher struct {
+ version string
+ pubkey string
+ privkey string
+ sharedSecret []byte
+ conversationKey [32]byte
+}
+
+func NewNip47Cipher(version, pubkey, privkey string) (*Nip47Cipher, error) {
+ _, err := isVersionSupported(version)
+ if err != nil {
+ return nil, err
+ }
+
+ var ss []byte
+ var ck [32]byte
+ if version == "0.0" {
+ ss, err = nip04.ComputeSharedSecret(pubkey, privkey)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ ck, err = nip44.GenerateConversationKey(pubkey, privkey)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return &Nip47Cipher{
+ version: version,
+ pubkey: pubkey,
+ privkey: privkey,
+ sharedSecret: ss,
+ conversationKey: ck,
+ }, nil
+}
+
+func (c *Nip47Cipher) Encrypt(message string) (msg string, err error) {
+ if c.version == "0.0" {
+ msg, err = nip04.Encrypt(message, c.sharedSecret)
+ if err != nil {
+ return "", err
+ }
+ } else {
+ msg, err = nip44.Encrypt(message, c.conversationKey)
+ if err != nil {
+ return "", err
+ }
+ }
+ return msg, nil
+}
+
+func (c *Nip47Cipher) Decrypt(content string) (payload string, err error) {
+ if c.version == "0.0" {
+ payload, err = nip04.Decrypt(content, c.sharedSecret)
+ if err != nil {
+ return "", err
+ }
+ } else {
+ payload, err = nip44.Decrypt(content, c.conversationKey)
+ if err != nil {
+ return "", err
+ }
+ }
+ return payload, nil
+}
+
+func isVersionSupported(version string) (bool, error) {
+ if version == "1.0" || version == "0.0" {
+ return true, nil
+ }
+
+ return false, fmt.Errorf("invalid version: %s", version)
+}
diff --git a/nip47/cipher/cipher_test.go b/nip47/cipher/cipher_test.go
new file mode 100644
index 000000000..51a802c91
--- /dev/null
+++ b/nip47/cipher/cipher_test.go
@@ -0,0 +1,47 @@
+package cipher
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/nbd-wtf/go-nostr"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCipher(t *testing.T) {
+ doTestCipher(t, "0.0")
+ doTestCipher(t, "1.0")
+}
+
+func doTestCipher(t *testing.T, version string) {
+ reqPrivateKey := nostr.GeneratePrivateKey()
+ reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
+
+ nip47Cipher, err := NewNip47Cipher(version, reqPubkey, reqPrivateKey)
+ assert.NoError(t, err)
+
+ payload := "test payload"
+ msg, err := nip47Cipher.Encrypt(payload)
+ assert.NoError(t, err)
+
+ decrypted, err := nip47Cipher.Decrypt(msg)
+ assert.Equal(t, payload, decrypted)
+}
+
+func TestCipher_UnsupportedVersions(t *testing.T) {
+ doTestCipher_UnsupportedVersions(t, "1")
+ doTestCipher_UnsupportedVersions(t, "x.1")
+ doTestCipher_UnsupportedVersions(t, "1.x")
+ doTestCipher_UnsupportedVersions(t, "2.0")
+ doTestCipher_UnsupportedVersions(t, "0.5")
+}
+
+func doTestCipher_UnsupportedVersions(t *testing.T, version string) {
+ reqPrivateKey := nostr.GeneratePrivateKey()
+ reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
+
+ _, err = NewNip47Cipher(version, reqPubkey, reqPrivateKey)
+ assert.Error(t, err)
+ assert.Equal(t, fmt.Sprintf("invalid version: %s", version), err.Error())
+}
diff --git a/nip47/controllers/get_balance_controller.go b/nip47/controllers/get_balance_controller.go
index 72e702234..18d342fdb 100644
--- a/nip47/controllers/get_balance_controller.go
+++ b/nip47/controllers/get_balance_controller.go
@@ -17,7 +17,7 @@ const (
)
type getBalanceResponse struct {
- Balance uint64 `json:"balance"`
+ Balance int64 `json:"balance"`
// MaxAmount int `json:"max_amount"`
// BudgetRenewal string `json:"budget_renewal"`
}
@@ -29,12 +29,12 @@ func (controller *nip47Controller) HandleGetBalanceEvent(ctx context.Context, ni
"request_event_id": requestEventId,
}).Debug("Getting balance")
- balance := uint64(0)
+ balance := int64(0)
if app.Isolated {
balance = queries.GetIsolatedBalance(controller.db, app.ID)
} else {
balances, err := controller.lnClient.GetBalances(ctx)
- balance = uint64(balances.Lightning.TotalSpendable)
+ balance = balances.Lightning.TotalSpendable
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"request_event_id": requestEventId,
diff --git a/nip47/controllers/get_balance_controller_test.go b/nip47/controllers/get_balance_controller_test.go
index 1db1f09cb..551211376 100644
--- a/nip47/controllers/get_balance_controller_test.go
+++ b/nip47/controllers/get_balance_controller_test.go
@@ -51,7 +51,7 @@ func TestHandleGetBalanceEvent(t *testing.T) {
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
- assert.Equal(t, uint64(21000), publishedResponse.Result.(*getBalanceResponse).Balance)
+ assert.Equal(t, int64(21000), publishedResponse.Result.(*getBalanceResponse).Balance)
assert.Nil(t, publishedResponse.Error)
}
@@ -85,7 +85,7 @@ func TestHandleGetBalanceEvent_IsolatedApp_NoTransactions(t *testing.T) {
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
- assert.Equal(t, uint64(0), publishedResponse.Result.(*getBalanceResponse).Balance)
+ assert.Equal(t, int64(0), publishedResponse.Result.(*getBalanceResponse).Balance)
assert.Nil(t, publishedResponse.Error)
}
func TestHandleGetBalanceEvent_IsolatedApp_Transactions(t *testing.T) {
@@ -132,6 +132,6 @@ func TestHandleGetBalanceEvent_IsolatedApp_Transactions(t *testing.T) {
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
- assert.Equal(t, uint64(1000), publishedResponse.Result.(*getBalanceResponse).Balance)
+ assert.Equal(t, int64(1000), publishedResponse.Result.(*getBalanceResponse).Balance)
assert.Nil(t, publishedResponse.Error)
}
diff --git a/nip47/event_handler.go b/nip47/event_handler.go
index 1d0c2de49..b8a4e748a 100644
--- a/nip47/event_handler.go
+++ b/nip47/event_handler.go
@@ -13,12 +13,12 @@ import (
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/logger"
+ "github.com/getAlby/hub/nip47/cipher"
"github.com/getAlby/hub/nip47/controllers"
"github.com/getAlby/hub/nip47/models"
"github.com/getAlby/hub/nip47/permissions"
nostrmodels "github.com/getAlby/hub/nostr/models"
"github.com/nbd-wtf/go-nostr"
- "github.com/nbd-wtf/go-nostr/nip04"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
@@ -93,12 +93,21 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
}
}
- ss, err := nip04.ComputeSharedSecret(app.AppPubkey, appWalletPrivKey)
+ version := "0.0"
+ vTag := event.Tags.GetFirst([]string{"v"})
+
+ if vTag != nil && vTag.Value() != "" {
+ version = vTag.Value()
+ }
+
+ nip47Cipher, err := cipher.NewNip47Cipher(version, app.AppPubkey, appWalletPrivKey)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"requestEventNostrId": event.ID,
"eventKind": event.Kind,
- }).WithError(err).Error("Failed to compute shared secret")
+ "appId": app.ID,
+ "version": version,
+ }).WithError(err).Error("Failed to initialize cipher")
requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR
err = svc.db.Save(&requestEvent).Error
@@ -123,7 +132,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
Message: fmt.Sprintf("Failed to save app to nostr event: %s", err.Error()),
},
}
- resp, err := svc.CreateResponse(event, nip47Response, nostr.Tags{}, ss, appWalletPrivKey)
+ resp, err := svc.CreateResponse(event, nip47Response, nostr.Tags{}, nip47Cipher, appWalletPrivKey)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"requestEventNostrId": event.ID,
@@ -143,17 +152,13 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
return
}
- payload, err := nip04.Decrypt(event.Content, ss)
+ payload, err := nip47Cipher.Decrypt(event.Content)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"requestEventNostrId": event.ID,
"eventKind": event.Kind,
"appId": app.ID,
}).WithError(err).Error("Failed to decrypt content")
- logger.Logger.WithFields(logrus.Fields{
- "requestEventNostrId": event.ID,
- "eventKind": event.Kind,
- }).WithError(err).Error("Failed to decrypt request event")
requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR
err = svc.db.Save(&requestEvent).Error
@@ -191,7 +196,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
// TODO: replace with a channel
// TODO: update all previous occurences of svc.publishResponseEvent to also use the channel
publishResponse := func(nip47Response *models.Response, tags nostr.Tags) {
- resp, err := svc.CreateResponse(event, nip47Response, tags, ss, appWalletPrivKey)
+ resp, err := svc.CreateResponse(event, nip47Response, tags, nip47Cipher, appWalletPrivKey)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"requestEventNostrId": event.ID,
@@ -340,12 +345,13 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
}
}
-func (svc *nip47Service) CreateResponse(initialEvent *nostr.Event, content interface{}, tags nostr.Tags, ss []byte, appWalletPrivKey string) (result *nostr.Event, err error) {
+func (svc *nip47Service) CreateResponse(initialEvent *nostr.Event, content interface{}, tags nostr.Tags, cipher *cipher.Nip47Cipher, appWalletPrivKey string) (result *nostr.Event, err error) {
payloadBytes, err := json.Marshal(content)
if err != nil {
return nil, err
}
- msg, err := nip04.Encrypt(string(payloadBytes), ss)
+
+ msg, err := cipher.Encrypt(string(payloadBytes))
if err != nil {
return nil, err
}
diff --git a/nip47/event_handler_legacy_test.go b/nip47/event_handler_legacy_test.go
deleted file mode 100644
index 68b8c048d..000000000
--- a/nip47/event_handler_legacy_test.go
+++ /dev/null
@@ -1,192 +0,0 @@
-package nip47
-
-import (
- "context"
- "encoding/json"
- "slices"
- "testing"
-
- "github.com/getAlby/hub/constants"
- "github.com/getAlby/hub/db"
- "github.com/getAlby/hub/nip47/models"
- "github.com/getAlby/hub/nip47/permissions"
- "github.com/getAlby/hub/tests"
- "github.com/nbd-wtf/go-nostr"
- "github.com/nbd-wtf/go-nostr/nip04"
- "github.com/stretchr/testify/assert"
-)
-
-func TestHandleResponse_LegacyApp_WithPermission(t *testing.T) {
- defer tests.RemoveTestService()
- svc, err := tests.CreateTestService()
- assert.NoError(t, err)
- nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
-
- reqPrivateKey := nostr.GeneratePrivateKey()
- reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
- assert.NoError(t, err)
-
- app, ss, err := tests.CreateLegacyApp(svc, reqPrivateKey)
- assert.NoError(t, err)
-
- appPermission := &db.AppPermission{
- AppId: app.ID,
- App: *app,
- Scope: constants.GET_BALANCE_SCOPE,
- }
- err = svc.DB.Create(appPermission).Error
- assert.NoError(t, err)
-
- content := map[string]interface{}{
- "method": models.GET_INFO_METHOD,
- }
-
- payloadBytes, err := json.Marshal(content)
- assert.NoError(t, err)
-
- msg, err := nip04.Encrypt(string(payloadBytes), ss)
- assert.NoError(t, err)
-
- reqEvent := &nostr.Event{
- Kind: models.REQUEST_KIND,
- PubKey: reqPubkey,
- CreatedAt: nostr.Now(),
- Tags: nostr.Tags{},
- Content: msg,
- }
- err = reqEvent.Sign(reqPrivateKey)
- assert.NoError(t, err)
-
- relay := tests.NewMockRelay()
-
- nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
-
- assert.NotNil(t, relay.PublishedEvent)
- assert.NotEmpty(t, relay.PublishedEvent.Content)
-
- decrypted, err := nip04.Decrypt(relay.PublishedEvent.Content, ss)
- assert.NoError(t, err)
-
- type getInfoResult struct {
- Methods []string `json:"methods"`
- }
-
- type getInfoResponseWrapper struct {
- models.Response
- Result getInfoResult `json:"result"`
- }
-
- unmarshalledResponse := getInfoResponseWrapper{}
-
- err = json.Unmarshal([]byte(decrypted), &unmarshalledResponse)
- assert.NoError(t, err)
- assert.Nil(t, unmarshalledResponse.Error)
- assert.Equal(t, models.GET_INFO_METHOD, unmarshalledResponse.ResultType)
- expectedMethods := slices.Concat([]string{constants.GET_BALANCE_SCOPE}, permissions.GetAlwaysGrantedMethods())
- assert.Equal(t, expectedMethods, unmarshalledResponse.Result.Methods)
-}
-
-func TestHandleResponse_LegacyApp_NoPermission(t *testing.T) {
- defer tests.RemoveTestService()
- svc, err := tests.CreateTestService()
- assert.NoError(t, err)
- nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
-
- reqPrivateKey := nostr.GeneratePrivateKey()
- reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
- assert.NoError(t, err)
-
- _, ss, err := tests.CreateLegacyApp(svc, reqPrivateKey)
- assert.NoError(t, err)
-
- content := map[string]interface{}{
- "method": models.GET_BALANCE_METHOD,
- }
-
- payloadBytes, err := json.Marshal(content)
- assert.NoError(t, err)
-
- msg, err := nip04.Encrypt(string(payloadBytes), ss)
- assert.NoError(t, err)
-
- reqEvent := &nostr.Event{
- Kind: models.REQUEST_KIND,
- PubKey: reqPubkey,
- CreatedAt: nostr.Now(),
- Tags: nostr.Tags{},
- Content: msg,
- }
- err = reqEvent.Sign(reqPrivateKey)
- assert.NoError(t, err)
-
- relay := tests.NewMockRelay()
-
- nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
-
- assert.NotNil(t, relay.PublishedEvent)
- assert.NotEmpty(t, relay.PublishedEvent.Content)
-
- decrypted, err := nip04.Decrypt(relay.PublishedEvent.Content, ss)
- assert.NoError(t, err)
-
- unmarshalledResponse := models.Response{}
-
- err = json.Unmarshal([]byte(decrypted), &unmarshalledResponse)
- assert.NoError(t, err)
- assert.Nil(t, unmarshalledResponse.Result)
- assert.Equal(t, models.GET_BALANCE_METHOD, unmarshalledResponse.ResultType)
- assert.Equal(t, "RESTRICTED", unmarshalledResponse.Error.Code)
- assert.Equal(t, "This app does not have the get_balance scope", unmarshalledResponse.Error.Message)
-}
-
-func TestHandleResponse_LegacyApp_IncorrectPubkey(t *testing.T) {
- defer tests.RemoveTestService()
- svc, err := tests.CreateTestService()
- assert.NoError(t, err)
- nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
-
- reqPrivateKey := nostr.GeneratePrivateKey()
- reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
- assert.NoError(t, err)
-
- reqPrivateKey2 := nostr.GeneratePrivateKey()
-
- app, ss, err := tests.CreateLegacyApp(svc, reqPrivateKey)
- assert.NoError(t, err)
-
- appPermission := &db.AppPermission{
- AppId: app.ID,
- App: *app,
- Scope: constants.GET_BALANCE_SCOPE,
- }
- err = svc.DB.Create(appPermission).Error
- assert.NoError(t, err)
-
- content := map[string]interface{}{
- "method": models.GET_BALANCE_METHOD,
- }
-
- payloadBytes, err := json.Marshal(content)
- assert.NoError(t, err)
-
- msg, err := nip04.Encrypt(string(payloadBytes), ss)
- assert.NoError(t, err)
-
- reqEvent := &nostr.Event{
- Kind: models.REQUEST_KIND,
- CreatedAt: nostr.Now(),
- Tags: nostr.Tags{},
- Content: msg,
- }
- err = reqEvent.Sign(reqPrivateKey2)
- assert.NoError(t, err)
-
- // set a different pubkey (this will not pass validation)
- reqEvent.PubKey = reqPubkey
-
- relay := tests.NewMockRelay()
-
- nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
-
- assert.Nil(t, relay.PublishedEvent)
-}
diff --git a/nip47/event_handler_shared_wallet_pubkey_test.go b/nip47/event_handler_shared_wallet_pubkey_test.go
new file mode 100644
index 000000000..ddb15f5fb
--- /dev/null
+++ b/nip47/event_handler_shared_wallet_pubkey_test.go
@@ -0,0 +1,88 @@
+package nip47
+
+import (
+ "testing"
+
+ "github.com/getAlby/hub/tests"
+ "github.com/stretchr/testify/require"
+)
+
+func TestHandleResponse_SharedWalletPubkey_Nip04_WithPermission(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ require.NoError(t, err)
+
+ doTestHandleResponse_WithPermission(t, svc, tests.CreateAppWithSharedWalletPubkey, "0.0")
+}
+
+func TestHandleResponse_SharedWalletPubkey_Nip44_WithPermission(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ require.NoError(t, err)
+
+ doTestHandleResponse_WithPermission(t, svc, tests.CreateAppWithSharedWalletPubkey, "1.0")
+}
+
+func TestHandleResponse_SharedWalletPubkey_Nip04_DuplicateRequest(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ require.NoError(t, err)
+
+ doTestHandleResponse_DuplicateRequest(t, svc, tests.CreateAppWithSharedWalletPubkey, "0.0")
+}
+
+func TestHandleResponse_SharedWalletPubkey_Nip44_DuplicateRequest(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ require.NoError(t, err)
+
+ doTestHandleResponse_DuplicateRequest(t, svc, tests.CreateAppWithSharedWalletPubkey, "1.0")
+}
+
+func TestHandleResponse_SharedWalletPubkey_Nip04_NoPermission(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ require.NoError(t, err)
+
+ doTestHandleResponse_NoPermission(t, svc, tests.CreateAppWithSharedWalletPubkey, "0.0")
+}
+
+func TestHandleResponse_SharedWalletPubkey_Nip44_NoPermission(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ require.NoError(t, err)
+
+ doTestHandleResponse_NoPermission(t, svc, tests.CreateAppWithSharedWalletPubkey, "1.0")
+}
+
+func TestHandleResponse_SharedWalletPubkey_Nip04_IncorrectPubkey(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ require.NoError(t, err)
+
+ doTestHandleResponse_IncorrectPubkey(t, svc, tests.CreateAppWithSharedWalletPubkey, "0.0")
+}
+
+func TestHandleResponse_SharedWalletPubkey_Nip44_IncorrectPubkey(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ require.NoError(t, err)
+
+ doTestHandleResponse_IncorrectPubkey(t, svc, tests.CreateAppWithSharedWalletPubkey, "1.0")
+}
+
+func TestHandleResponse_SharedWalletPubkey_Nip04_OldRequestForPayment(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ require.NoError(t, err)
+
+ doTestHandleResponse_OldRequestForPayment(t, svc, tests.CreateAppWithSharedWalletPubkey, "0.0")
+}
+
+func TestHandleResponse_SharedWalletPubkey_Nip44_OldRequestForPayment(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ require.NoError(t, err)
+
+ doTestHandleResponse_OldRequestForPayment(t, svc, tests.CreateAppWithSharedWalletPubkey, "1.0")
+}
diff --git a/nip47/event_handler_test.go b/nip47/event_handler_test.go
index 342f8fc93..84f7adeb3 100644
--- a/nip47/event_handler_test.go
+++ b/nip47/event_handler_test.go
@@ -9,11 +9,11 @@ import (
"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
+ "github.com/getAlby/hub/nip47/cipher"
"github.com/getAlby/hub/nip47/models"
"github.com/getAlby/hub/nip47/permissions"
"github.com/getAlby/hub/tests"
"github.com/nbd-wtf/go-nostr"
- "github.com/nbd-wtf/go-nostr/nip04"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -22,11 +22,23 @@ import (
// 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) {
+func TestCreateResponse_Nip04(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
+ doTestCreateResponse(t, svc, "0.0")
+}
+
+func TestCreateResponse_Nip44(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ require.NoError(t, err)
+
+ doTestCreateResponse(t, svc, "1.0")
+}
+
+func doTestCreateResponse(t *testing.T, svc *tests.TestService, nip47Version string) {
reqPrivateKey := nostr.GeneratePrivateKey()
reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
assert.NoError(t, err)
@@ -39,7 +51,7 @@ func TestCreateResponse(t *testing.T) {
reqEvent.ID = "12345"
- ss, err := nip04.ComputeSharedSecret(reqPubkey, svc.Keys.GetNostrSecretKey())
+ nip47Cipher, err := cipher.NewNip47Cipher(nip47Version, reqPubkey, svc.Keys.GetNostrSecretKey())
assert.NoError(t, err)
type dummyResponse struct {
@@ -55,13 +67,13 @@ func TestCreateResponse(t *testing.T) {
nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
- res, err := nip47svc.CreateResponse(reqEvent, nip47Response, nostr.Tags{}, ss, svc.Keys.GetNostrSecretKey())
+ res, err := nip47svc.CreateResponse(reqEvent, nip47Response, nostr.Tags{}, nip47Cipher, svc.Keys.GetNostrSecretKey())
assert.NoError(t, err)
assert.Equal(t, reqPubkey, res.Tags.GetFirst([]string{"p"}).Value())
assert.Equal(t, reqEvent.ID, res.Tags.GetFirst([]string{"e"}).Value())
assert.Equal(t, svc.Keys.GetNostrPublicKey(), res.PubKey)
- decrypted, err := nip04.Decrypt(res.Content, ss)
+ decrypted, err := nip47Cipher.Decrypt(res.Content)
assert.NoError(t, err)
unmarshalledResponse := models.Response{
Result: &dummyResponse{},
@@ -74,17 +86,30 @@ func TestCreateResponse(t *testing.T) {
assert.Equal(t, nip47Response.Result, *unmarshalledResponse.Result.(*dummyResponse))
}
-func TestHandleResponse_WithPermission(t *testing.T) {
+func TestHandleResponse_Nip04_WithPermission(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
+
+ doTestHandleResponse_WithPermission(t, svc, tests.CreateAppWithPrivateKey, "0.0")
+}
+
+func TestHandleResponse_Nip44_WithPermission(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ require.NoError(t, err)
+
+ doTestHandleResponse_WithPermission(t, svc, tests.CreateAppWithPrivateKey, "1.0")
+}
+
+func doTestHandleResponse_WithPermission(t *testing.T, svc *tests.TestService, createAppFn tests.CreateAppFn, version string) {
nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
reqPrivateKey := nostr.GeneratePrivateKey()
reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
assert.NoError(t, err)
- app, ss, err := tests.CreateAppWithPrivateKey(svc, reqPrivateKey)
+ app, cipher, err := createAppFn(svc, reqPrivateKey, version)
assert.NoError(t, err)
appPermission := &db.AppPermission{
@@ -102,7 +127,7 @@ func TestHandleResponse_WithPermission(t *testing.T) {
payloadBytes, err := json.Marshal(content)
assert.NoError(t, err)
- msg, err := nip04.Encrypt(string(payloadBytes), ss)
+ msg, err := cipher.Encrypt(string(payloadBytes))
assert.NoError(t, err)
reqEvent := &nostr.Event{
@@ -112,6 +137,11 @@ func TestHandleResponse_WithPermission(t *testing.T) {
Tags: nostr.Tags{},
Content: msg,
}
+
+ if version != "0.0" {
+ reqEvent.Tags = append(reqEvent.Tags, []string{"v", version})
+ }
+
err = reqEvent.Sign(reqPrivateKey)
assert.NoError(t, err)
@@ -119,10 +149,10 @@ func TestHandleResponse_WithPermission(t *testing.T) {
nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
- assert.NotNil(t, relay.PublishedEvent)
- assert.NotEmpty(t, relay.PublishedEvent.Content)
+ assert.NotNil(t, relay.PublishedEvents[0])
+ assert.NotEmpty(t, relay.PublishedEvents[0].Content)
- decrypted, err := nip04.Decrypt(relay.PublishedEvent.Content, ss)
+ decrypted, err := cipher.Decrypt(relay.PublishedEvents[0].Content)
assert.NoError(t, err)
type getInfoResult struct {
@@ -144,17 +174,30 @@ func TestHandleResponse_WithPermission(t *testing.T) {
assert.Equal(t, expectedMethods, unmarshalledResponse.Result.Methods)
}
-func TestHandleResponse_DuplicateRequest(t *testing.T) {
+func TestHandleResponse_Nip04_DuplicateRequest(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ require.NoError(t, err)
+
+ doTestHandleResponse_DuplicateRequest(t, svc, tests.CreateAppWithPrivateKey, "0.0")
+}
+
+func TestHandleResponse_Nip44_DuplicateRequest(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
+
+ doTestHandleResponse_DuplicateRequest(t, svc, tests.CreateAppWithPrivateKey, "1.0")
+}
+
+func doTestHandleResponse_DuplicateRequest(t *testing.T, svc *tests.TestService, createAppFn tests.CreateAppFn, version string) {
nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
reqPrivateKey := nostr.GeneratePrivateKey()
reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
assert.NoError(t, err)
- app, ss, err := tests.CreateAppWithPrivateKey(svc, reqPrivateKey)
+ app, cipher, err := createAppFn(svc, reqPrivateKey, version)
assert.NoError(t, err)
appPermission := &db.AppPermission{
@@ -172,7 +215,7 @@ func TestHandleResponse_DuplicateRequest(t *testing.T) {
payloadBytes, err := json.Marshal(content)
assert.NoError(t, err)
- msg, err := nip04.Encrypt(string(payloadBytes), ss)
+ msg, err := cipher.Encrypt(string(payloadBytes))
assert.NoError(t, err)
reqEvent := &nostr.Event{
@@ -182,6 +225,11 @@ func TestHandleResponse_DuplicateRequest(t *testing.T) {
Tags: nostr.Tags{},
Content: msg,
}
+
+ if version != "0.0" {
+ reqEvent.Tags = append(reqEvent.Tags, []string{"v", version})
+ }
+
err = reqEvent.Sign(reqPrivateKey)
assert.NoError(t, err)
@@ -189,28 +237,41 @@ func TestHandleResponse_DuplicateRequest(t *testing.T) {
nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
- assert.NotNil(t, relay.PublishedEvent)
- assert.NotEmpty(t, relay.PublishedEvent.Content)
+ assert.NotNil(t, relay.PublishedEvents[0])
+ assert.NotEmpty(t, relay.PublishedEvents[0].Content)
- relay.PublishedEvent = nil
+ relay.PublishedEvents = nil
nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
// second time it should not publish
- assert.Nil(t, relay.PublishedEvent)
+ assert.Nil(t, relay.PublishedEvents)
}
-func TestHandleResponse_NoPermission(t *testing.T) {
+func TestHandleResponse_Nip04_NoPermission(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
+
+ doTestHandleResponse_NoPermission(t, svc, tests.CreateAppWithPrivateKey, "0.0")
+}
+
+func TestHandleResponse_Nip44_NoPermission(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ require.NoError(t, err)
+
+ doTestHandleResponse_NoPermission(t, svc, tests.CreateAppWithPrivateKey, "1.0")
+}
+
+func doTestHandleResponse_NoPermission(t *testing.T, svc *tests.TestService, createAppFn tests.CreateAppFn, version string) {
nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
reqPrivateKey := nostr.GeneratePrivateKey()
reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
assert.NoError(t, err)
- _, ss, err := tests.CreateAppWithPrivateKey(svc, reqPrivateKey)
+ _, cipher, err := createAppFn(svc, reqPrivateKey, version)
assert.NoError(t, err)
content := map[string]interface{}{
@@ -220,7 +281,7 @@ func TestHandleResponse_NoPermission(t *testing.T) {
payloadBytes, err := json.Marshal(content)
assert.NoError(t, err)
- msg, err := nip04.Encrypt(string(payloadBytes), ss)
+ msg, err := cipher.Encrypt(string(payloadBytes))
assert.NoError(t, err)
reqEvent := &nostr.Event{
@@ -230,6 +291,11 @@ func TestHandleResponse_NoPermission(t *testing.T) {
Tags: nostr.Tags{},
Content: msg,
}
+
+ if version != "0.0" {
+ reqEvent.Tags = append(reqEvent.Tags, []string{"v", version})
+ }
+
err = reqEvent.Sign(reqPrivateKey)
assert.NoError(t, err)
@@ -237,10 +303,10 @@ func TestHandleResponse_NoPermission(t *testing.T) {
nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
- assert.NotNil(t, relay.PublishedEvent)
- assert.NotEmpty(t, relay.PublishedEvent.Content)
+ assert.NotNil(t, relay.PublishedEvents[0])
+ assert.NotEmpty(t, relay.PublishedEvents[0].Content)
- decrypted, err := nip04.Decrypt(relay.PublishedEvent.Content, ss)
+ decrypted, err := cipher.Decrypt(relay.PublishedEvents[0].Content)
assert.NoError(t, err)
unmarshalledResponse := models.Response{}
@@ -253,40 +319,62 @@ func TestHandleResponse_NoPermission(t *testing.T) {
assert.Equal(t, "This app does not have the get_balance scope", unmarshalledResponse.Error.Message)
}
-func TestHandleResponse_NoApp(t *testing.T) {
+func TestHandleResponse_Nip04_OldRequestForPayment(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
+
+ doTestHandleResponse_OldRequestForPayment(t, svc, tests.CreateAppWithPrivateKey, "0.0")
+}
+
+func TestHandleResponse_Nip44_OldRequestForPayment(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ require.NoError(t, err)
+
+ doTestHandleResponse_OldRequestForPayment(t, svc, tests.CreateAppWithPrivateKey, "1.0")
+}
+
+func doTestHandleResponse_OldRequestForPayment(t *testing.T, svc *tests.TestService, createAppFn tests.CreateAppFn, version string) {
nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
reqPrivateKey := nostr.GeneratePrivateKey()
reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
assert.NoError(t, err)
- app, ss, err := tests.CreateAppWithPrivateKey(svc, reqPrivateKey)
- assert.NoError(t, err)
-
- // delete the app
- err = svc.DB.Delete(app).Error
+ app, cipher, err := createAppFn(svc, reqPrivateKey, version)
assert.NoError(t, err)
content := map[string]interface{}{
- "method": models.GET_BALANCE_METHOD,
+ "method": models.PAY_INVOICE_METHOD,
+ }
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
}
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
payloadBytes, err := json.Marshal(content)
assert.NoError(t, err)
- msg, err := nip04.Encrypt(string(payloadBytes), ss)
+ msg, err := cipher.Encrypt(string(payloadBytes))
assert.NoError(t, err)
reqEvent := &nostr.Event{
Kind: models.REQUEST_KIND,
PubKey: reqPubkey,
- CreatedAt: nostr.Now(),
+ CreatedAt: nostr.Timestamp(time.Now().Add(time.Duration(-6) * time.Hour).Unix()),
Tags: nostr.Tags{},
Content: msg,
}
+
+ if version != "0.0" {
+ reqEvent.Tags = append(reqEvent.Tags, []string{"v", version})
+ }
+
err = reqEvent.Sign(reqPrivateKey)
assert.NoError(t, err)
@@ -294,80 +382,152 @@ func TestHandleResponse_NoApp(t *testing.T) {
nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
- // it shouldn't return anything for an invalid app key
- assert.Nil(t, relay.PublishedEvent)
+ // it shouldn't return anything for an old request
+ assert.Nil(t, relay.PublishedEvents)
+
+ // change the request to now
+ reqEvent.CreatedAt = nostr.Now()
+ err = reqEvent.Sign(reqPrivateKey)
+ assert.NoError(t, err)
+
+ nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
+ assert.NotNil(t, relay.PublishedEvents)
+}
+
+func TestHandleResponse_Nip04_IncorrectPubkey(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ require.NoError(t, err)
+
+ doTestHandleResponse_IncorrectPubkey(t, svc, tests.CreateAppWithPrivateKey, "0.0")
}
-func TestHandleResponse_OldRequestForPayment(t *testing.T) {
+func TestHandleResponse_Nip44_IncorrectPubkey(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
+
+ doTestHandleResponse_IncorrectPubkey(t, svc, tests.CreateAppWithPrivateKey, "1.0")
+}
+
+func doTestHandleResponse_IncorrectPubkey(t *testing.T, svc *tests.TestService, createAppFn tests.CreateAppFn, version string) {
nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
reqPrivateKey := nostr.GeneratePrivateKey()
reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
assert.NoError(t, err)
- app, ss, err := tests.CreateAppWithPrivateKey(svc, reqPrivateKey)
- assert.NoError(t, err)
+ reqPrivateKey2 := nostr.GeneratePrivateKey()
- content := map[string]interface{}{
- "method": models.PAY_INVOICE_METHOD,
- }
+ app, cipher, err := createAppFn(svc, reqPrivateKey, version)
+ assert.NoError(t, err)
appPermission := &db.AppPermission{
AppId: app.ID,
App: *app,
- Scope: constants.PAY_INVOICE_SCOPE,
+ Scope: constants.GET_BALANCE_SCOPE,
}
err = svc.DB.Create(appPermission).Error
assert.NoError(t, err)
+ content := map[string]interface{}{
+ "method": models.GET_BALANCE_METHOD,
+ }
+
payloadBytes, err := json.Marshal(content)
assert.NoError(t, err)
- msg, err := nip04.Encrypt(string(payloadBytes), ss)
+ msg, err := cipher.Encrypt(string(payloadBytes))
assert.NoError(t, err)
reqEvent := &nostr.Event{
Kind: models.REQUEST_KIND,
- PubKey: reqPubkey,
- CreatedAt: nostr.Timestamp(time.Now().Add(time.Duration(-6) * time.Hour).Unix()),
+ CreatedAt: nostr.Now(),
Tags: nostr.Tags{},
Content: msg,
}
- err = reqEvent.Sign(reqPrivateKey)
+
+ if version != "0.0" {
+ reqEvent.Tags = append(reqEvent.Tags, []string{"v", version})
+ }
+
+ err = reqEvent.Sign(reqPrivateKey2)
assert.NoError(t, err)
+ // set a different pubkey (this will not pass validation)
+ reqEvent.PubKey = reqPubkey
+
relay := tests.NewMockRelay()
nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
- // it shouldn't return anything for an old request
- assert.Nil(t, relay.PublishedEvent)
+ assert.Nil(t, relay.PublishedEvents)
+}
- // change the request to now
- reqEvent.CreatedAt = nostr.Now()
+func TestHandleResponse_NoApp(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ require.NoError(t, err)
+ nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
+
+ reqPrivateKey := nostr.GeneratePrivateKey()
+ reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
+ assert.NoError(t, err)
+
+ app, cipher, err := tests.CreateAppWithPrivateKey(svc, reqPrivateKey, "1.0")
+ assert.NoError(t, err)
+
+ // delete the app
+ err = svc.DB.Delete(app).Error
+ assert.NoError(t, err)
+
+ content := map[string]interface{}{
+ "method": models.GET_BALANCE_METHOD,
+ }
+
+ payloadBytes, err := json.Marshal(content)
+ assert.NoError(t, err)
+
+ msg, err := cipher.Encrypt(string(payloadBytes))
+ assert.NoError(t, err)
+
+ reqEvent := &nostr.Event{
+ Kind: models.REQUEST_KIND,
+ PubKey: reqPubkey,
+ CreatedAt: nostr.Now(),
+ Tags: nostr.Tags{[]string{"v", "1.0"}},
+ Content: msg,
+ }
err = reqEvent.Sign(reqPrivateKey)
assert.NoError(t, err)
+ relay := tests.NewMockRelay()
+
nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
- assert.NotNil(t, relay.PublishedEvent)
+
+ // it shouldn't return anything for an invalid app key
+ assert.Nil(t, relay.PublishedEvents)
}
-func TestHandleResponse_IncorrectPubkey(t *testing.T) {
+func TestHandleResponse_IncorrectVersions(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
+ // version specifies what cipher will use. If "1.0" is passed,
+ // cipher must be NIP-44, otherwise cipher MUST be NIP-04
+ doTestHandleResponse_IncorrectVersion(t, svc, "0.0", "1.0")
+ doTestHandleResponse_IncorrectVersion(t, svc, "1.0", "0.0")
+ doTestHandleResponse_IncorrectVersion(t, svc, "1.0", "")
+}
+
+func doTestHandleResponse_IncorrectVersion(t *testing.T, svc *tests.TestService, appVersion, requestVersion string) {
nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
reqPrivateKey := nostr.GeneratePrivateKey()
reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
assert.NoError(t, err)
- reqPrivateKey2 := nostr.GeneratePrivateKey()
-
- app, ss, err := tests.CreateAppWithPrivateKey(svc, reqPrivateKey)
+ app, cipher, err := tests.CreateAppWithPrivateKey(svc, reqPrivateKey, appVersion)
assert.NoError(t, err)
appPermission := &db.AppPermission{
@@ -379,30 +539,35 @@ func TestHandleResponse_IncorrectPubkey(t *testing.T) {
assert.NoError(t, err)
content := map[string]interface{}{
- "method": models.GET_BALANCE_METHOD,
+ "method": models.GET_INFO_METHOD,
}
payloadBytes, err := json.Marshal(content)
assert.NoError(t, err)
- msg, err := nip04.Encrypt(string(payloadBytes), ss)
+ msg, err := cipher.Encrypt(string(payloadBytes))
assert.NoError(t, err)
+ // don't pass correct version
reqEvent := &nostr.Event{
Kind: models.REQUEST_KIND,
+ PubKey: reqPubkey,
CreatedAt: nostr.Now(),
Tags: nostr.Tags{},
Content: msg,
}
- err = reqEvent.Sign(reqPrivateKey2)
- assert.NoError(t, err)
- // set a different pubkey (this will not pass validation)
- reqEvent.PubKey = reqPubkey
+ if requestVersion != "" {
+ reqEvent.Tags = append(reqEvent.Tags, []string{"v", requestVersion})
+ }
+
+ err = reqEvent.Sign(reqPrivateKey)
+ assert.NoError(t, err)
relay := tests.NewMockRelay()
nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
- assert.Nil(t, relay.PublishedEvent)
+ // it shouldn't return anything for an invalid version
+ assert.Nil(t, relay.PublishedEvents)
}
diff --git a/nip47/models/models.go b/nip47/models/models.go
index 05ad61873..6c7be0b05 100644
--- a/nip47/models/models.go
+++ b/nip47/models/models.go
@@ -5,10 +5,11 @@ import (
)
const (
- INFO_EVENT_KIND = 13194
- REQUEST_KIND = 23194
- RESPONSE_KIND = 23195
- NOTIFICATION_KIND = 23196
+ INFO_EVENT_KIND = 13194
+ REQUEST_KIND = 23194
+ RESPONSE_KIND = 23195
+ LEGACY_NOTIFICATION_KIND = 23196
+ NOTIFICATION_KIND = 23197
// request methods
PAY_INVOICE_METHOD = "pay_invoice"
diff --git a/nip47/nip47_service.go b/nip47/nip47_service.go
index 3079179d8..21710b225 100644
--- a/nip47/nip47_service.go
+++ b/nip47/nip47_service.go
@@ -2,9 +2,11 @@ package nip47
import (
"context"
+
"github.com/getAlby/hub/config"
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/lnclient"
+ "github.com/getAlby/hub/nip47/cipher"
"github.com/getAlby/hub/nip47/notifications"
"github.com/getAlby/hub/nip47/permissions"
nostrmodels "github.com/getAlby/hub/nostr/models"
@@ -31,7 +33,7 @@ type Nip47Service interface {
GetNip47Info(ctx context.Context, relay *nostr.Relay, appWalletPubKey string) (*nostr.Event, error)
PublishNip47Info(ctx context.Context, relay nostrmodels.Relay, appWalletPubKey string, appWalletPrivKey string, lnClient lnclient.LNClient) (*nostr.Event, error)
PublishNip47InfoDeletion(ctx context.Context, relay nostrmodels.Relay, appWalletPubKey string, appWalletPrivKey string, infoEventId string) error
- CreateResponse(initialEvent *nostr.Event, content interface{}, tags nostr.Tags, ss []byte, walletPrivKey string) (result *nostr.Event, err error)
+ CreateResponse(initialEvent *nostr.Event, content interface{}, tags nostr.Tags, cipher *cipher.Nip47Cipher, walletPrivKey string) (result *nostr.Event, err error)
}
func NewNip47Service(db *gorm.DB, cfg config.Config, keys keys.Keys, eventPublisher events.EventPublisher) *nip47Service {
diff --git a/nip47/notifications/nip47_notifier.go b/nip47/notifications/nip47_notifier.go
index 9c8a9d15e..f8fa23824 100644
--- a/nip47/notifications/nip47_notifier.go
+++ b/nip47/notifications/nip47_notifier.go
@@ -10,13 +10,13 @@ import (
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/logger"
+ "github.com/getAlby/hub/nip47/cipher"
"github.com/getAlby/hub/nip47/models"
"github.com/getAlby/hub/nip47/permissions"
nostrmodels "github.com/getAlby/hub/nostr/models"
"github.com/getAlby/hub/service/keys"
"github.com/getAlby/hub/transactions"
"github.com/nbd-wtf/go-nostr"
- "github.com/nbd-wtf/go-nostr/nip04"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
@@ -98,52 +98,68 @@ func (notifier *Nip47Notifier) notifySubscribers(ctx context.Context, notificati
if !hasPermission {
continue
}
- notifier.notifySubscriber(ctx, &app, notification, tags)
- }
-}
-func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App, notification *Notification, tags nostr.Tags) {
- logger.Logger.WithFields(logrus.Fields{
- "notification": notification,
- "appId": app.ID,
- }).Debug("Notifying subscriber")
-
- var err error
+ appWalletPrivKey := notifier.keys.GetNostrSecretKey()
+ if app.WalletPubkey != nil {
+ appWalletPrivKey, err = notifier.keys.GetAppWalletKey(app.ID)
+ if err != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "notification": notification,
+ "appId": app.ID,
+ }).WithError(err).Error("error deriving child key")
+ return
+ }
+ }
- appWalletPrivKey := notifier.keys.GetNostrSecretKey()
- if app.WalletPubkey != nil {
- appWalletPrivKey, err = notifier.keys.GetAppWalletKey(app.ID)
+ appWalletPubKey, err := nostr.GetPublicKey(appWalletPrivKey)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"notification": notification,
"appId": app.ID,
- }).WithError(err).Error("error deriving child key")
+ }).WithError(err).Error("Failed to calculate app wallet pub key")
return
}
+
+ notifier.notifySubscriber(ctx, &app, notification, tags, appWalletPubKey, appWalletPrivKey, "0.0")
+ notifier.notifySubscriber(ctx, &app, notification, tags, appWalletPubKey, appWalletPrivKey, "1.0")
}
+}
+
+func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App, notification *Notification, tags nostr.Tags, appWalletPubKey, appWalletPrivKey string, version string) {
+ logger.Logger.WithFields(logrus.Fields{
+ "notification": notification,
+ "appId": app.ID,
+ "version": version,
+ }).Debug("Notifying subscriber")
- ss, err := nip04.ComputeSharedSecret(app.AppPubkey, appWalletPrivKey)
+ var err error
+
+ payloadBytes, err := json.Marshal(notification)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"notification": notification,
"appId": app.ID,
- }).WithError(err).Error("Failed to compute shared secret")
+ "version": version,
+ }).WithError(err).Error("Failed to stringify notification")
return
}
- payloadBytes, err := json.Marshal(notification)
+ nip47Cipher, err := cipher.NewNip47Cipher(version, app.AppPubkey, appWalletPrivKey)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"notification": notification,
"appId": app.ID,
- }).WithError(err).Error("Failed to stringify notification")
+ "version": version,
+ }).WithError(err).Error("Failed to initialize cipher")
return
}
- msg, err := nip04.Encrypt(string(payloadBytes), ss)
+
+ msg, err := nip47Cipher.Encrypt(string(payloadBytes))
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"notification": notification,
"appId": app.ID,
+ "version": version,
}).WithError(err).Error("Failed to encrypt notification payload")
return
}
@@ -151,15 +167,6 @@ func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App
allTags := nostr.Tags{[]string{"p", app.AppPubkey}}
allTags = append(allTags, tags...)
- appWalletPubKey, err := nostr.GetPublicKey(appWalletPrivKey)
- if err != nil {
- logger.Logger.WithFields(logrus.Fields{
- "notification": notification,
- "appId": app.ID,
- }).WithError(err).Error("Failed to calculate app wallet pub key")
- return
- }
-
event := &nostr.Event{
PubKey: appWalletPubKey,
CreatedAt: nostr.Now(),
@@ -167,11 +174,17 @@ func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App
Tags: allTags,
Content: msg,
}
+
+ if version == "0.0" {
+ event.Kind = models.LEGACY_NOTIFICATION_KIND
+ }
+
err = event.Sign(appWalletPrivKey)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"notification": notification,
"appId": app.ID,
+ "version": version,
}).WithError(err).Error("Failed to sign event")
return
}
@@ -181,10 +194,12 @@ func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App
logger.Logger.WithFields(logrus.Fields{
"notification": notification,
"appId": app.ID,
+ "version": version,
}).WithError(err).Error("Failed to publish notification")
return
}
logger.Logger.WithFields(logrus.Fields{
- "appId": app.ID,
+ "appId": app.ID,
+ "version": version,
}).Debug("Published notification event")
}
diff --git a/nip47/notifications/nip47_notifier_test.go b/nip47/notifications/nip47_notifier_test.go
index 6f5142556..962edb7db 100644
--- a/nip47/notifications/nip47_notifier_test.go
+++ b/nip47/notifications/nip47_notifier_test.go
@@ -3,10 +3,11 @@ package notifications
import (
"context"
"encoding/json"
- "github.com/nbd-wtf/go-nostr"
"testing"
"time"
+ "github.com/nbd-wtf/go-nostr"
+
"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/events"
@@ -14,7 +15,6 @@ import (
"github.com/getAlby/hub/nip47/permissions"
"github.com/getAlby/hub/tests"
"github.com/getAlby/hub/transactions"
- "github.com/nbd-wtf/go-nostr/nip04"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -33,15 +33,18 @@ func (svc *mockConsumer) ConsumeEvent(ctx context.Context, event *events.Event,
svc.nip47NotificationQueue.AddToQueue(event)
}
-func doTestSendNotificationPaymentReceived(t *testing.T, svc *tests.TestService, app *db.App, ss []byte) {
+func doTestSendNotificationPaymentReceived(t *testing.T, svc *tests.TestService, createAppFn tests.CreateAppFn, nip47Version string) {
ctx := context.TODO()
+ app, cipher, err := createAppFn(svc, nostr.GeneratePrivateKey(), nip47Version)
+ assert.NoError(t, err)
+
appPermission := &db.AppPermission{
AppId: app.ID,
App: *app,
Scope: constants.NOTIFICATIONS_SCOPE,
}
- err := svc.DB.Create(appPermission).Error
+ err = svc.DB.Create(appPermission).Error
assert.NoError(t, err)
settledAt := time.Unix(*tests.MockLNClientTransaction.SettledAt, 0)
@@ -82,10 +85,17 @@ func doTestSendNotificationPaymentReceived(t *testing.T, svc *tests.TestService,
notifier := NewNip47Notifier(relay, svc.DB, svc.Cfg, svc.Keys, permissionsSvc, transactionsSvc, svc.LNClient)
notifier.ConsumeEvent(ctx, receivedEvent)
- assert.NotNil(t, relay.PublishedEvent)
- assert.NotEmpty(t, relay.PublishedEvent.Content)
+ var publishedEvent *nostr.Event
+ if nip47Version == "0.0" {
+ publishedEvent = relay.PublishedEvents[0]
+ } else {
+ publishedEvent = relay.PublishedEvents[1]
+ }
+
+ assert.NotNil(t, publishedEvent)
+ assert.NotEmpty(t, publishedEvent.Content)
- decrypted, err := nip04.Decrypt(relay.PublishedEvent.Content, ss)
+ decrypted, err := cipher.Decrypt(publishedEvent.Content)
assert.NoError(t, err)
unmarshalledResponse := Notification{
Notification: &PaymentReceivedNotification{},
@@ -107,35 +117,50 @@ func doTestSendNotificationPaymentReceived(t *testing.T, svc *tests.TestService,
assert.Equal(t, tests.MockLNClientTransaction.SettledAt, transaction.SettledAt)
}
-func TestSendNotification_PaymentReceived(t *testing.T) {
+func TestSendNotification_Nip04_PaymentReceived(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
- app, ss, err := tests.CreateApp(svc)
- assert.NoError(t, err)
- doTestSendNotificationPaymentReceived(t, svc, app, ss)
+ doTestSendNotificationPaymentReceived(t, svc, tests.CreateAppWithPrivateKey, "0.0")
}
-func TestSendNotification_Legacy_PaymentReceived(t *testing.T) {
+func TestSendNotification_Nip44_PaymentReceived(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
- app, ss, err := tests.CreateLegacyApp(svc, nostr.GeneratePrivateKey())
- assert.NoError(t, err)
- doTestSendNotificationPaymentReceived(t, svc, app, ss)
+ doTestSendNotificationPaymentReceived(t, svc, tests.CreateAppWithPrivateKey, "1.0")
+}
+
+func TestSendNotification_SharedWalletPubkey_Nip04_PaymentReceived(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ require.NoError(t, err)
+
+ doTestSendNotificationPaymentReceived(t, svc, tests.CreateAppWithSharedWalletPubkey, "0.0")
+}
+
+func TestSendNotification_SharedWalletPubkey_Nip44_PaymentReceived(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ require.NoError(t, err)
+
+ doTestSendNotificationPaymentReceived(t, svc, tests.CreateAppWithSharedWalletPubkey, "1.0")
}
-func doTestSendNotificationPaymentSent(t *testing.T, svc *tests.TestService, app *db.App, ss []byte) {
+func doTestSendNotificationPaymentSent(t *testing.T, svc *tests.TestService, createAppFn tests.CreateAppFn, nip47Version string) {
ctx := context.TODO()
+ app, cipher, err := createAppFn(svc, nostr.GeneratePrivateKey(), nip47Version)
+ assert.NoError(t, err)
+
appPermission := &db.AppPermission{
AppId: app.ID,
App: *app,
Scope: constants.NOTIFICATIONS_SCOPE,
}
- err := svc.DB.Create(appPermission).Error
+ err = svc.DB.Create(appPermission).Error
assert.NoError(t, err)
settledAt := time.Unix(*tests.MockLNClientTransaction.SettledAt, 0)
@@ -175,10 +200,17 @@ func doTestSendNotificationPaymentSent(t *testing.T, svc *tests.TestService, app
notifier := NewNip47Notifier(relay, svc.DB, svc.Cfg, svc.Keys, permissionsSvc, transactionsSvc, svc.LNClient)
notifier.ConsumeEvent(ctx, receivedEvent)
- assert.NotNil(t, relay.PublishedEvent)
- assert.NotEmpty(t, relay.PublishedEvent.Content)
+ var publishedEvent *nostr.Event
+ if nip47Version == "0.0" {
+ publishedEvent = relay.PublishedEvents[0]
+ } else {
+ publishedEvent = relay.PublishedEvents[1]
+ }
+
+ assert.NotNil(t, publishedEvent)
+ assert.NotEmpty(t, publishedEvent.Content)
- decrypted, err := nip04.Decrypt(relay.PublishedEvent.Content, ss)
+ decrypted, err := cipher.Decrypt(publishedEvent.Content)
assert.NoError(t, err)
unmarshalledResponse := Notification{
Notification: &PaymentReceivedNotification{},
@@ -200,24 +232,36 @@ func doTestSendNotificationPaymentSent(t *testing.T, svc *tests.TestService, app
assert.Equal(t, tests.MockLNClientTransaction.SettledAt, transaction.SettledAt)
}
-func TestSendNotification_PaymentSent(t *testing.T) {
+func TestSendNotification_Nip04_PaymentSent(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
- app, ss, err := tests.CreateApp(svc)
- assert.NoError(t, err)
- doTestSendNotificationPaymentSent(t, svc, app, ss)
+ doTestSendNotificationPaymentSent(t, svc, tests.CreateAppWithPrivateKey, "0.0")
}
-func TestSendNotification_Legacy_PaymentSent(t *testing.T) {
+func TestSendNotification_Nip44_PaymentSent(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
- app, ss, err := tests.CreateLegacyApp(svc, nostr.GeneratePrivateKey())
- assert.NoError(t, err)
- doTestSendNotificationPaymentSent(t, svc, app, ss)
+ doTestSendNotificationPaymentSent(t, svc, tests.CreateAppWithPrivateKey, "1.0")
+}
+
+func TestSendNotification_SharedWalletPubkey_Nip04_PaymentSent(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ require.NoError(t, err)
+
+ doTestSendNotificationPaymentSent(t, svc, tests.CreateAppWithSharedWalletPubkey, "0.0")
+}
+
+func TestSendNotification_SharedWalletPubkey_Nip44_PaymentSent(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ require.NoError(t, err)
+
+ doTestSendNotificationPaymentSent(t, svc, tests.CreateAppWithSharedWalletPubkey, "1.0")
}
func doTestSendNotificationNoPermission(t *testing.T, svc *tests.TestService) {
@@ -250,22 +294,23 @@ func doTestSendNotificationNoPermission(t *testing.T, svc *tests.TestService) {
notifier := NewNip47Notifier(relay, svc.DB, svc.Cfg, svc.Keys, permissionsSvc, transactionsSvc, svc.LNClient)
notifier.ConsumeEvent(ctx, receivedEvent)
- assert.Nil(t, relay.PublishedEvent)
+ assert.Nil(t, relay.PublishedEvents)
}
func TestSendNotification_NoPermission(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
- _, _, err = tests.CreateApp(svc)
+ _, _, err = tests.CreateAppWithPrivateKey(svc, nostr.GeneratePrivateKey(), "1.0")
assert.NoError(t, err)
doTestSendNotificationNoPermission(t, svc)
}
-func TestSendNotification_Legacy_NoPermission(t *testing.T) {
+
+func TestSendNotification_SharedWalletPubkey_NoPermission(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
- _, _, err = tests.CreateLegacyApp(svc, nostr.GeneratePrivateKey())
+ _, _, err = tests.CreateAppWithSharedWalletPubkey(svc, nostr.GeneratePrivateKey(), "1.0")
assert.NoError(t, err)
doTestSendNotificationNoPermission(t, svc)
}
diff --git a/nip47/publish_nip47_info.go b/nip47/publish_nip47_info.go
index ca2e848aa..4477a90c2 100644
--- a/nip47/publish_nip47_info.go
+++ b/nip47/publish_nip47_info.go
@@ -9,6 +9,7 @@ import (
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/logger"
+ "github.com/getAlby/hub/nip47/cipher"
"github.com/getAlby/hub/nip47/models"
nostrmodels "github.com/getAlby/hub/nostr/models"
"github.com/nbd-wtf/go-nostr"
@@ -37,7 +38,7 @@ func (svc *nip47Service) GetNip47Info(ctx context.Context, relay *nostr.Relay, a
func (svc *nip47Service) PublishNip47Info(ctx context.Context, relay nostrmodels.Relay, appWalletPubKey string, appWalletPrivKey string, lnClient lnclient.LNClient) (*nostr.Event, error) {
var capabilities []string
var permitsNotifications bool
- tags := nostr.Tags{[]string{}}
+ tags := nostr.Tags{[]string{"v", cipher.SUPPORTED_VERSIONS}}
if svc.keys.GetNostrPublicKey() == appWalletPubKey {
// legacy app, so return lnClient.GetSupportedNIP47Methods()
capabilities = lnClient.GetSupportedNIP47Methods()
diff --git a/scripts/keys/rolznz.asc b/scripts/keys/rolznz.asc
new file mode 100644
index 000000000..60a58ef70
--- /dev/null
+++ b/scripts/keys/rolznz.asc
@@ -0,0 +1,52 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQINBGd9NCYBEACvWntWPwrBopLMee+v86pmokiXfrh6d6YtU9xiOdOff7nGPMT9
+HywWgL2G2CUlUch8CyQcna69BEB5qkZjGo8VC6+rKclc9RRg9wPGUV4XhGMZH8CM
+VJYQ/yifJXCi2f5pRcboL2X/o+2ht2XxUl4Rp4YbWrl5+GZJspKPyKLGVjQLj3TY
+5CXuTxNFPUjve8m630+yW/XGsnSXTJAYDJ9YZP99iFeAJT3aLkUlJxfKOuPb3aJS
+Luz/9NDZwFAhv9NhRjSGcNtBG91UcO2l4aMfVsdKvrLThbqT3aXXdDBgVD0nlM2r
+hYgVgWisNKXp7UJFppSDOZJuBiRNElpnDxYiNNf54At5ulzejS1JHEfVu06DiX+M
+l1u8QaqVTz8Qne8mUV7cxDz6zQb52ezn2VE/jzbgD478NrMSzCc+Z8ODunRfUQ0R
+vrgswu5Cfqt6cXkltCVXY3SQOMigiPsPmU8PLXNtrnX3QDaDMXRYUkiJ/187UhZn
+bag8MH9GFeQI85oq0OadQsJm9zn87bwl7fF8YCNZjDZ+v3gtu/WUVTnHyT1kToIy
+Athzwo7ZNqgKopqDKtobziPpVU6HW3ExAcdu1F/6VuXkJ/Uw1fOrWaUr7EmV51GA
+aprEzbbjv+3OoJZmTuGSucO5DtzJDRn+BaDgwkXxHQ583EpvFmzsj1dy4QARAQAB
+tCdSb2xhbmQgQmV3aWNrIDxyb2xhbmQuYmV3aWNrQGdtYWlsLmNvbT6JAlQEEwEK
+AD4WIQRdkhhZOObb+JPczFul6r2INQkrCAUCZ300JgIbAwUJA8JnAAULCQgHAgYV
+CgkICwIEFgIDAQIeAQIXgAAKCRCl6r2INQkrCH92D/0TgbA+PI3jqA9RMSyPMXVQ
+hvHoET6wfbOXVKKgxY5IpOgqOqgmbJg15vaUJNw3YgMbtlACkGWQPoHf4iKMOmZ4
+A4Yn/VyDCMTgP25gGlaZb99uhpA5e1pDm5fiAwgfaX/VDUWpca7a6kgCxy+UaoWs
+CwW9j15FWAlI6rQMsp/xj4CHoj4xajGzP8RVNw3WrIhJqFRN5uWl0iw0VEw/BqqP
+oy+GUl3Ycvc73fQs9gogq6XLHBnP9dp0gKQMPVQwhNxZTbm/srHL2+vceQ2bSjQC
+5HSewqTv+LtYf4MQWqXSabeFF4Ih8j+Ed0Ulmai9hi0al8HgIH4qbHGJjAJQ4m61
+hRrwAukfk99mHEJ4tN5nKJiSU1WEYAgvOmqIAbh8zpJgzUlJuvYFLvBbsDD7x7Y2
+/IVRyxHvNxNUUDnsgs83Mk2aqEJ1N9i/6O8uhHg/cbnXmNCgA/lKY2RwDQ1avHGL
+X164kpii405mgx+RZfdlLTX3aTI/IBAI5S5vVpwfawTjznonwEr+MAf/WJsZupOc
+4aBMKDkWR4Hlu2gpX9JOrpWwdvPo8KbZIDYrPAHe1Lza+tGZJIisC76XaMc/IJOy
+GrevdDCorS2YOnpKZQfR+KiOujmG6pPdVp54EfsNPRKmFHkULoDFVPNI/cDUGZA+
+U2qcjnYrzSo/iWAIeMAOQrkCDQRnfTQmARAA0s8voN1S5pCd/3a2wzurEhiZQcHG
+h6xMlNvFfRrdqf3Ix4FfIsBlAWK47ldqaPMif6c0YUPjavOBUGNC54/PMbcRiTTl
+K2X1LfqZwtJ5oDQBOw59Ti/e+9oNMHPLKPWX5UXM0s7aEfXyiXPtqJNqeL/sSTKM
+FWEIh07jvDzuJu3VSYYv0T8CpOp7hCOVa1XXdIHBQgfXt0+yw/Gh139//2/t8w2B
+BJgL6qOrCecWYXreIDmzG4E0nEs0qeDJSSd51Il9yMeC2DVrg2Z1oI72G/NIpJlQ
+YMwTG90uFIgNfwiM5hBP03L7x3upcsvOxiCGPv1/emG/0y5lyXDsF84lhG9NJO9p
+ZXZyPzNjtsm+beNwZWLBKxjccfrfuVpmEfr1pv7uock27rPGtY48upddEj70mFGN
+xJ+GtMebCcojl4ZXidQV3rZHi4n4mjfgHZyABrupLLtZa5Eh26H9GGUNkh1AT+kf
+haFBDwjl3Fx2zIwZMqGvZMHYpU5n3HWNddl/Z8ZbQcfUkUOIVnc67rdKhdvlEosl
+o563ON+/dzbqApomI8pToKIKlYw9pEjppSlED0Vo7L5Bp0VgtZfG7v1N8CqQqSij
+Uk8qmL4bd8n3jreg1SoTHWcwYG8gPsKCZlALZzCSaGU+XyU78EU9bfmqK19IgMvd
+sexuXWzzAJO0JYkAEQEAAYkCPAQYAQoAJhYhBF2SGFk45tv4k9zMW6XqvYg1CSsI
+BQJnfTQmAhsMBQkDwmcAAAoJEKXqvYg1CSsITSkP/1QEGGucOKq8ycxLk63C+mTA
+hYW/xUrac8um4VaEs8t4Egr5zF05IgQqDGDWLOx3p9kPnTK/z+gSiOZk+xs0JGpC
+scDwp25CiJeG54E2mYKvUTuAljBaWCia5mRHz4CL7yLjcFHuM3hSBPcT8/tDFkbG
+P3ESyr54o9SjvDbUV6SrcQwuTYafFYPx5E/bise6ZY9pH1ZnvFNwWUYnFDuy27uJ
+ii8Xev2r5M8fk+AxXlBkuMQQ/K48sps7mNFi8gDCZf26+OYkr9otLTyvDZuCLUVZ
+hVSq4HyY4cpE4NMhk4hGu2yS3VXiWNX0FoyBSZeWT7Q5EbZQpVR9xcAQAlq+9AEo
+xar1h1YBUfeEHB+JdA89UKRhHJd3fgnLxdALOYovFVGE7tuguIlvMdk/sRdTVTom
+OC86X7BJRrM7PSX9/I8qGah8LH/B1PapGJyblGjbCkN3wlgXvFSweCae7Q/3coI2
+lp1dNWNUpK1Zv6BFUdD25gjULhT+uaO1e3q+lAkiBRd0iou16gCi5Ubtm9QMjbzD
+nkzNWGwVYVkM76zRpf+z66xro+zgf77Wds25bAPTwoj3py/51YaVjzCqSlfKIHq3
+fgOS4Sx2Jhm1cKa4QDyjHkd9d/yiYhaxASwQ2ogT9PY6Q17ycbanOpPMhGR1e+Eh
+6jouyYQFNCcTZxNL1vgc
+=RYUy
+-----END PGP PUBLIC KEY BLOCK-----
\ No newline at end of file
diff --git a/service/service.go b/service/service.go
index 9b729c93a..f6bf0dce8 100644
--- a/service/service.go
+++ b/service/service.go
@@ -6,6 +6,7 @@ import (
"path/filepath"
"strings"
"sync"
+ "sync/atomic"
"github.com/adrg/xdg"
"github.com/nbd-wtf/go-nostr"
@@ -41,6 +42,7 @@ type service struct {
nip47Service nip47.Nip47Service
appCancelFn context.CancelFunc
keys keys.Keys
+ isRelayReady atomic.Bool
}
func NewService(ctx context.Context) (*service, error) {
@@ -94,6 +96,18 @@ func NewService(ctx context.Context) (*service, error) {
return nil, err
}
+ // write auto unlock password from env to user config
+ if appConfig.AutoUnlockPassword != "" {
+ err = cfg.SetUpdate("AutoUnlockPassword", appConfig.AutoUnlockPassword, "")
+ if err != nil {
+ return nil, err
+ }
+ }
+ autoUnlockPassword, err := cfg.Get("AutoUnlockPassword", "")
+ if err != nil {
+ return nil, err
+ }
+
eventPublisher := events.NewEventPublisher()
keys := keys.NewKeys()
@@ -130,10 +144,10 @@ func NewService(ctx context.Context) (*service, error) {
startDataDogProfiler(ctx)
}
- if appConfig.AutoUnlockPassword != "" {
+ if autoUnlockPassword != "" {
nodeLastStartTime, _ := cfg.Get("NodeLastStartTime", "")
if nodeLastStartTime != "" {
- svc.StartApp(appConfig.AutoUnlockPassword)
+ svc.StartApp(autoUnlockPassword)
}
}
diff --git a/service/start.go b/service/start.go
index ca27ea574..12d97d971 100644
--- a/service/start.go
+++ b/service/start.go
@@ -44,7 +44,7 @@ func (svc *service) startNostr(ctx context.Context) error {
go func() {
// ensure the relay is properly disconnected before exiting
defer svc.wg.Done()
- //Start infinite loop which will be only broken by canceling ctx (SIGINT)
+ // Start infinite loop which will be only broken by canceling ctx (SIGINT)
var relay *nostr.Relay
waitToReconnectSeconds := 0
var createAppEventListener events.EventSubscriber
@@ -53,11 +53,13 @@ func (svc *service) startNostr(ctx context.Context) error {
// wait for a delay if any before retrying
contextCancelled := false
+ svc.setRelayReady(false)
+
select {
case <-ctx.Done(): // application service context cancelled
logger.Logger.Info("service context cancelled")
contextCancelled = true
- case <-time.After(time.Duration(waitToReconnectSeconds) * time.Second): //timeout
+ case <-time.After(time.Duration(waitToReconnectSeconds) * time.Second): // timeout
}
if contextCancelled {
break
@@ -65,7 +67,7 @@ func (svc *service) startNostr(ctx context.Context) error {
closeRelay(relay)
- //connect to the relay
+ // connect to the relay
logger.Logger.WithFields(logrus.Fields{
"relay_url": relayUrl,
"iteration": i,
@@ -124,16 +126,19 @@ func (svc *service) startNostr(ctx context.Context) error {
// to ensure we do not get duplicate events
err = svc.startAppWalletSubscription(ctx, relay, svc.keys.GetNostrPublicKey())
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.
+ // err being non-nil means that we have an error on the websocket error channel. In this case we just try to reconnect.
logger.Logger.WithError(err).Error("Got an error from the relay while listening to subscription.")
continue
}
}
+
+ svc.setRelayReady(true)
+
select {
case <-ctx.Done():
logger.Logger.Info("Main context cancelled, exiting...")
case <-relay.Context().Done():
- //err being non-nil means that we have an error on the websocket error channel. In this case we just try to reconnect.
+ // err being non-nil means that we have an error on the websocket error channel. In this case we just try to reconnect.
if relay.ConnectionError != nil {
logger.Logger.WithError(relay.ConnectionError).Error("Got an error from the relay, trying to reconnect")
} else {
@@ -157,7 +162,15 @@ func (svc *service) startAllExistingAppsWalletSubscriptions(ctx context.Context,
for _, app := range apps {
go func(app db.App) {
- err := svc.startAppWalletSubscription(ctx, relay, *app.WalletPubkey)
+ // republish info event for all existing apps
+ walletPrivKey, err := svc.keys.GetAppWalletKey(app.ID)
+ _, err = svc.GetNip47Service().PublishNip47Info(ctx, relay, *app.WalletPubkey, walletPrivKey, svc.lnClient)
+ if err != nil {
+ logger.Logger.WithError(err).WithFields(logrus.Fields{
+ "app_id": app.ID}).Error("Could not publish NIP47 info")
+ }
+
+ err = svc.startAppWalletSubscription(ctx, relay, *app.WalletPubkey)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"app_id": app.ID}).Error("Subscription error")
@@ -211,6 +224,14 @@ func (svc *service) StartSubscription(ctx context.Context, sub *nostr.Subscripti
}
func (svc *service) StartApp(encryptionKey string) error {
+ albyIdentifier, err := svc.albyOAuthSvc.GetUserIdentifier()
+ if err != nil {
+ return err
+ }
+ if albyIdentifier != "" && !svc.albyOAuthSvc.IsConnected(svc.ctx) {
+ return errors.New("alby account is not authenticated")
+ }
+
if svc.lnClient != nil {
return errors.New("app already started")
}
@@ -221,7 +242,7 @@ func (svc *service) StartApp(encryptionKey string) error {
ctx, cancelFn := context.WithCancel(svc.ctx)
- err := svc.keys.Init(svc.cfg, encryptionKey)
+ err = svc.keys.Init(svc.cfg, encryptionKey)
if err != nil {
logger.Logger.WithError(err).Error("Failed to init nostr keys")
cancelFn()
@@ -409,3 +430,11 @@ func (svc *service) requestVssToken(ctx context.Context) (string, error) {
}
return vssToken, nil
}
+
+func (svc *service) setRelayReady(ready bool) {
+ svc.isRelayReady.Store(ready)
+}
+
+func (svc *service) IsRelayReady() bool {
+ return svc.isRelayReady.Load()
+}
diff --git a/tests/create_app.go b/tests/create_app.go
index 775ff548f..09ba929c7 100644
--- a/tests/create_app.go
+++ b/tests/create_app.go
@@ -5,15 +5,18 @@ import (
db "github.com/getAlby/hub/db"
"github.com/getAlby/hub/events"
+ "github.com/getAlby/hub/nip47/cipher"
"github.com/nbd-wtf/go-nostr"
- "github.com/nbd-wtf/go-nostr/nip04"
"gorm.io/gorm"
)
-func CreateApp(svc *TestService) (app *db.App, ss []byte, err error) {
- return CreateAppWithPrivateKey(svc, "")
+type CreateAppFn func(svc *TestService, senderPrivkey string, nip47Version string) (app *db.App, nip47Cipher *cipher.Nip47Cipher, err error)
+
+func CreateApp(svc *TestService) (app *db.App, cipher *cipher.Nip47Cipher, err error) {
+ return CreateAppWithPrivateKey(svc, "", "1.0")
}
-func CreateAppWithPrivateKey(svc *TestService, senderPrivkey string) (app *db.App, ss []byte, err error) {
+
+func CreateAppWithPrivateKey(svc *TestService, senderPrivkey, nip47Version string) (app *db.App, nip47Cipher *cipher.Nip47Cipher, err error) {
senderPubkey := ""
if senderPrivkey != "" {
var err error
@@ -29,15 +32,15 @@ func CreateAppWithPrivateKey(svc *TestService, senderPrivkey string) (app *db.Ap
pairingSecretKey = senderPrivkey
}
- ss, err = nip04.ComputeSharedSecret(*app.WalletPubkey, pairingSecretKey)
+ nip47Cipher, err = cipher.NewNip47Cipher(nip47Version, *app.WalletPubkey, pairingSecretKey)
if err != nil {
return nil, nil, err
}
- return app, ss, nil
+ return app, nip47Cipher, nil
}
-func CreateLegacyApp(svc *TestService, senderPrivkey string) (app *db.App, ss []byte, err error) {
+func CreateAppWithSharedWalletPubkey(svc *TestService, senderPrivkey, nip47Version string) (app *db.App, nip47Cipher *cipher.Nip47Cipher, err error) {
pairingPublicKey, _ := nostr.GetPublicKey(senderPrivkey)
@@ -65,6 +68,6 @@ func CreateLegacyApp(svc *TestService, senderPrivkey string) (app *db.App, ss []
},
})
- ss, err = nip04.ComputeSharedSecret(svc.Keys.GetNostrPublicKey(), senderPrivkey)
- return app, ss, nil
+ nip47Cipher, err = cipher.NewNip47Cipher(nip47Version, svc.Keys.GetNostrPublicKey(), senderPrivkey)
+ return app, nip47Cipher, nil
}
diff --git a/tests/create_mock_relay.go b/tests/create_mock_relay.go
index b28f6a8fb..01f2e440d 100644
--- a/tests/create_mock_relay.go
+++ b/tests/create_mock_relay.go
@@ -8,7 +8,7 @@ import (
)
type mockRelay struct {
- PublishedEvent *nostr.Event
+ PublishedEvents []*nostr.Event
}
func NewMockRelay() *mockRelay {
@@ -17,6 +17,6 @@ func NewMockRelay() *mockRelay {
func (relay *mockRelay) Publish(ctx context.Context, event nostr.Event) error {
logger.Logger.WithField("event", event).Info("Mock Publishing event")
- relay.PublishedEvent = &event
+ relay.PublishedEvents = append(relay.PublishedEvents, &event)
return nil
}
diff --git a/transactions/isolated_app_payments_test.go b/transactions/isolated_app_payments_test.go
index f29dc466a..6936ce169 100644
--- a/transactions/isolated_app_payments_test.go
+++ b/transactions/isolated_app_payments_test.go
@@ -323,14 +323,9 @@ func TestSendPaymentSync_IsolatedApp_BalanceSufficient_FailedPayment(t *testing.
}
func TestCalculateFeeReserve(t *testing.T) {
- defer tests.RemoveTestService()
- svc, err := tests.CreateTestService()
- require.NoError(t, err)
- transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
-
- assert.Equal(t, uint64(10_000), transactionsService.calculateFeeReserveMsat(0))
- assert.Equal(t, uint64(10_000), transactionsService.calculateFeeReserveMsat(10_000))
- assert.Equal(t, uint64(10_000), transactionsService.calculateFeeReserveMsat(100_000))
- assert.Equal(t, uint64(10_000), transactionsService.calculateFeeReserveMsat(1000_000))
- assert.Equal(t, uint64(20_000), transactionsService.calculateFeeReserveMsat(2000_000))
+ assert.Equal(t, uint64(10_000), CalculateFeeReserveMsat(0))
+ assert.Equal(t, uint64(10_000), CalculateFeeReserveMsat(10_000))
+ assert.Equal(t, uint64(10_000), CalculateFeeReserveMsat(100_000))
+ assert.Equal(t, uint64(10_000), CalculateFeeReserveMsat(1000_000))
+ assert.Equal(t, uint64(20_000), CalculateFeeReserveMsat(2000_000))
}
diff --git a/transactions/keysend_test.go b/transactions/keysend_test.go
index 94302827f..e673c3ed8 100644
--- a/transactions/keysend_test.go
+++ b/transactions/keysend_test.go
@@ -418,7 +418,7 @@ func TestSendKeysend_IsolatedAppToNoApp(t *testing.T) {
result := svc.DB.Find(&transactions)
assert.Equal(t, int64(3), result.RowsAffected)
// expect balance to be decreased
- assert.Equal(t, uint64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
+ assert.Equal(t, int64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
}
func TestSendKeysend_IsolatedAppToIsolatedApp(t *testing.T) {
@@ -510,10 +510,10 @@ func TestSendKeysend_IsolatedAppToIsolatedApp(t *testing.T) {
result := svc.DB.Find(&transactions)
assert.Equal(t, int64(3), result.RowsAffected)
// expect balance to be decreased
- assert.Equal(t, uint64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
+ assert.Equal(t, int64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
// expect app2 to receive the payment
- assert.Equal(t, uint64(123000), queries.GetIsolatedBalance(svc.DB, app2.ID))
+ assert.Equal(t, int64(123000), queries.GetIsolatedBalance(svc.DB, app2.ID))
// check notifications
assert.Equal(t, 2, len(mockEventConsumer.GetConsumedEvents()))
diff --git a/transactions/self_payments_test.go b/transactions/self_payments_test.go
index 32aa71961..ba255f9f1 100644
--- a/transactions/self_payments_test.go
+++ b/transactions/self_payments_test.go
@@ -106,7 +106,7 @@ func TestSendPaymentSync_SelfPayment_NoAppToIsolatedApp(t *testing.T) {
result := svc.DB.Find(&transactions)
assert.Equal(t, int64(2), result.RowsAffected)
// expect balance to be increased
- assert.Equal(t, uint64(123000), queries.GetIsolatedBalance(svc.DB, app.ID))
+ assert.Equal(t, int64(123000), queries.GetIsolatedBalance(svc.DB, app.ID))
}
func TestSendPaymentSync_SelfPayment_NoAppToApp(t *testing.T) {
@@ -229,7 +229,7 @@ func TestSendPaymentSync_SelfPayment_IsolatedAppToNoApp(t *testing.T) {
result := svc.DB.Find(&transactions)
assert.Equal(t, int64(3), result.RowsAffected)
// expect balance to be decreased
- assert.Equal(t, uint64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
+ assert.Equal(t, int64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
}
func TestSendPaymentSync_SelfPayment_IsolatedAppToApp(t *testing.T) {
@@ -305,7 +305,7 @@ func TestSendPaymentSync_SelfPayment_IsolatedAppToApp(t *testing.T) {
result := svc.DB.Find(&transactions)
assert.Equal(t, int64(3), result.RowsAffected)
// expect balance to be decreased
- assert.Equal(t, uint64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
+ assert.Equal(t, int64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
}
func TestSendPaymentSync_SelfPayment_IsolatedAppToIsolatedApp(t *testing.T) {
@@ -387,7 +387,7 @@ func TestSendPaymentSync_SelfPayment_IsolatedAppToIsolatedApp(t *testing.T) {
result := svc.DB.Find(&transactions)
assert.Equal(t, int64(3), result.RowsAffected)
// expect balance to be decreased
- assert.Equal(t, uint64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
+ assert.Equal(t, int64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
// check notifications
assert.Equal(t, 2, len(mockEventConsumer.GetConsumedEvents()))
@@ -473,5 +473,5 @@ func TestSendPaymentSync_SelfPayment_IsolatedAppToSelf(t *testing.T) {
assert.Equal(t, int64(3), result.RowsAffected)
// expect balance to be unchanged
- assert.Equal(t, uint64(133000), queries.GetIsolatedBalance(svc.DB, app.ID))
+ assert.Equal(t, int64(133000), queries.GetIsolatedBalance(svc.DB, app.ID))
}
diff --git a/transactions/transactions_service.go b/transactions/transactions_service.go
index 4f3805300..5e783eb37 100644
--- a/transactions/transactions_service.go
+++ b/transactions/transactions_service.go
@@ -250,7 +250,7 @@ func (svc *transactionsService) SendPaymentSync(ctx context.Context, payReq stri
RequestEventId: requestEventId,
Type: constants.TRANSACTION_TYPE_OUTGOING,
State: constants.TRANSACTION_STATE_PENDING,
- FeeReserveMsat: svc.calculateFeeReserveMsat(paymentAmount),
+ FeeReserveMsat: CalculateFeeReserveMsat(paymentAmount),
AmountMsat: paymentAmount,
PaymentRequest: payReq,
PaymentHash: paymentRequest.PaymentHash,
@@ -363,7 +363,7 @@ func (svc *transactionsService) SendKeysend(ctx context.Context, amount uint64,
RequestEventId: requestEventId,
Type: constants.TRANSACTION_TYPE_OUTGOING,
State: constants.TRANSACTION_STATE_PENDING,
- FeeReserveMsat: svc.calculateFeeReserveMsat(uint64(amount)),
+ FeeReserveMsat: CalculateFeeReserveMsat(uint64(amount)),
AmountMsat: amount,
Metadata: datatypes.JSON(metadataBytes),
Boostagram: datatypes.JSON(boostagramBytes),
@@ -796,7 +796,7 @@ func (svc *transactionsService) interceptSelfPayment(paymentHash string) (*lncli
}
func (svc *transactionsService) validateCanPay(tx *gorm.DB, appId *uint, amount uint64, description string) error {
- amountWithFeeReserve := amount + svc.calculateFeeReserveMsat(amount)
+ amountWithFeeReserve := amount + CalculateFeeReserveMsat(amount)
// ensure balance for isolated apps
if appId != nil {
@@ -820,7 +820,7 @@ func (svc *transactionsService) validateCanPay(tx *gorm.DB, appId *uint, amount
if app.Isolated {
balance := queries.GetIsolatedBalance(tx, appPermission.AppId)
- if amountWithFeeReserve > balance {
+ if int64(amountWithFeeReserve) > balance {
message := NewInsufficientBalanceError().Error()
if description != "" {
message += " " + description
@@ -862,9 +862,8 @@ func (svc *transactionsService) validateCanPay(tx *gorm.DB, appId *uint, amount
}
// max of 1% or 10000 millisats (10 sats)
-func (svc *transactionsService) calculateFeeReserveMsat(amount uint64) uint64 {
- // NOTE: LDK defaults to 1% of the payment amount + 50 sats
- return uint64(math.Max(math.Ceil(float64(amount)*0.01), 10000))
+func CalculateFeeReserveMsat(amountMsat uint64) uint64 {
+ return uint64(math.Max(math.Ceil(float64(amountMsat)*0.01), 10000))
}
func makePreimageHex() ([]byte, error) {
diff --git a/wails/wails_app.go b/wails/wails_app.go
index ffa0cbee5..298de22b8 100644
--- a/wails/wails_app.go
+++ b/wails/wails_app.go
@@ -13,6 +13,7 @@ import (
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"github.com/wailsapp/wails/v2/pkg/options/linux"
"github.com/wailsapp/wails/v2/pkg/options/mac"
+ "github.com/wailsapp/wails/v2/pkg/runtime"
"gorm.io/gorm"
)
@@ -39,6 +40,21 @@ func (app *WailsApp) startup(ctx context.Context) {
app.ctx = ctx
}
+func (app *WailsApp) onBeforeClose(ctx context.Context) bool {
+ response, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
+ Type: runtime.QuestionDialog,
+ Title: "Confirm Exit",
+ Message: "Are you sure you want to shut down Alby Hub? Alby Hub needs to stay online to send and receive transactions.",
+ Buttons: []string{"Yes", "No"},
+ DefaultButton: "No",
+ })
+ if err != nil {
+ logger.Logger.WithError(err).Error("failed to show confirmation dialog")
+ return false
+ }
+ return response != "Yes"
+}
+
func LaunchWailsApp(app *WailsApp, assets embed.FS, appIcon []byte) {
err := wails.Run(&options.App{
Title: "Alby Hub",
@@ -52,6 +68,9 @@ func LaunchWailsApp(app *WailsApp, assets embed.FS, appIcon []byte) {
//BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
OnStartup: app.startup,
+ OnBeforeClose: func(ctx context.Context) bool {
+ return app.onBeforeClose(ctx)
+ },
Bind: []interface{}{
app,
},
diff --git a/wails/wails_handlers.go b/wails/wails_handlers.go
index 3ca00cec6..f27744fe4 100644
--- a/wails/wails_handlers.go
+++ b/wails/wails_handlers.go
@@ -711,6 +711,28 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
}
return WailsRequestRouterResponse{Body: nil, Error: ""}
+ case "/api/auto-unlock":
+ autoUnlockRequest := &api.AutoUnlockRequest{}
+ err := json.Unmarshal([]byte(body), autoUnlockRequest)
+ if err != nil {
+ 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.SetAutoUnlockPassword(autoUnlockRequest.UnlockPassword)
+ if err != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "route": route,
+ "method": method,
+ "body": body,
+ }).WithError(err).Error("Failed to set auto unlock password")
+ return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
+ }
+ return WailsRequestRouterResponse{Body: nil, Error: ""}
case "/api/start":
startRequest := &api.StartRequest{}
err := json.Unmarshal([]byte(body), startRequest)