diff --git a/cmd/blindbit-scan/main.go b/cmd/blindbit-scan/main.go index c1024a6..cde1471 100644 --- a/cmd/blindbit-scan/main.go +++ b/cmd/blindbit-scan/main.go @@ -1,103 +1,31 @@ package main import ( - "bytes" - "context" - "flag" "os" "os/signal" - "github.com/setavenger/blindbit-scan/internal/config" - "github.com/setavenger/blindbit-scan/internal/daemon" - nwcserver "github.com/setavenger/blindbit-scan/internal/nwc_server" - "github.com/setavenger/blindbit-scan/internal/server" - "github.com/setavenger/blindbit-scan/pkg/database" - "github.com/setavenger/blindbit-scan/pkg/logging" - "github.com/setavenger/blindbit-scan/pkg/networking/nwc" + "github.com/setavenger/blindbit-scan/internal/startup" ) -func init() { - // todo can this double reference work? - flag.StringVar( - &config.DirectoryPath, - "datadir", - config.DefaultDirectoryPath, - "Set the base directory for blindbit-scan. Default directory is ~/.blindbit-scan.", - ) - flag.Parse() -} +// func init() { +// // todo can this double reference work? +// flag.StringVar( +// &config.DirectoryPath, +// "datadir", +// config.DefaultDirectoryPath, +// "Set the base directory for blindbit-scan. Default directory is ~/.blindbit-scan.", +// ) + +// flag.BoolVar(&config.PrivateMode, "private", false, "BlindBit Scan will run in private mode. All data on disk will be encrypted all data will only be decrypted in memory. Upon restart the unlock endpoint needs to be called to decrypt data and start the scanning.") + +// flag.Parse() +// } func main() { interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) - var err error - - // todo move to a go routine to avoid blocking - err = config.SetupConfigs(config.DirectoryPath) - if err != nil { - logging.L.Panic().Err(err). - Msg("startup failed, could not setup configs") - } - - var d *daemon.Daemon - - d, err = daemon.SetupDaemonNoWallet() - if err != nil { - logging.L.Panic().Err(err). - Msg("startup failed, could produce daemon hull") - } - w, err := database.TryLoadWalletFromDisk(config.PathDbWallet) - if err != nil { - logging.L.Warn().Err(err). - Msg("startup failed, could setup full daemon") - } - d.Wallet = w - - // Setup BlindBit Nostr Wallet Connect - nwcServer := nwcserver.NewNwcServer(d) - - logging.L.Info().Msg("attempting to load NWC apps from disk") - controller, err := database.TryLoadingControllerFromDisk(context.Background(), config.PathDbNWC) - if err != nil { - logging.L.Panic().Err(err).Msg("failed to create new controller") - } - logging.L.Trace().Any("apps", controller.Apps()).Msg("controller data") - - controller.RegisterHandler(nwc.GET_INFO_METHOD, nwcServer.GetInfoHandler()) - controller.RegisterHandler(nwc.GET_BALANCE_METHOD, nwcServer.GetBalanceHandler()) - controller.RegisterHandler(nwc.LIST_UTXOS_METHOD, nwcServer.ListUtxosHandler()) - - err = controller.ConnectRelay() - if err != nil { - logging.L.Panic().Err(err).Msg("failed to connect to relay") - } - go controller.StartListening() - - // http server - go func() { - err = server.StartNewServer(d, controller) - if err != nil { - logging.L.Panic().Err(err). - Msg("startup failed, could start server") - } - }() - - // when we exit we still flush the last state - defer d.SaveWalletToDB() - go func() { - // if the keys are not setup we wait - if d.Wallet == nil || bytes.Equal(d.Wallet.SecretKeyScan[:], make([]byte, 32)) || bytes.Equal(d.Wallet.PubKeySpend[:], make([]byte, 33)) { - logging.L.Info().Msg("waiting for keys") - <-config.KeysReadyChan - d, err = daemon.SetupDaemon(config.PathDbWallet) - if err != nil { - logging.L.Panic().Err(err). - Msg("startup failed, could setup full daemon") - } - } - go d.ContinuousScan() - }() + startup.RunProgram() // wait for program stop signal <-interrupt diff --git a/go.mod b/go.mod index a4d08c9..5e0ae51 100644 --- a/go.mod +++ b/go.mod @@ -8,11 +8,11 @@ require ( github.com/btcsuite/btcd/btcutil v1.1.6 github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 github.com/btcsuite/goleveldb v1.0.0 + github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0 github.com/gin-contrib/cors v1.7.2 github.com/gin-gonic/gin v1.10.0 github.com/nbd-wtf/go-nostr v0.50.0 github.com/rs/zerolog v1.33.0 - github.com/setavenger/blindbitd v0.0.0-20240602183715-c4e971bba3e4 github.com/setavenger/go-bip352 v0.1.7 github.com/setavenger/go-electrum v1.1.1 github.com/spf13/viper v1.19.0 @@ -69,7 +69,7 @@ require ( golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/net v0.37.0 // indirect golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/text v0.24.0 // indirect google.golang.org/protobuf v1.36.2 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 7670f05..bdde8ad 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0 h1:aYo8nnk3ojoQkP5iErif5Xxv0Mo0Ga/FR5+ffl/7+Nk= +github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -160,8 +162,6 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/setavenger/blindbitd v0.0.0-20240602183715-c4e971bba3e4 h1:mKbdSQiWncNMS2AExIYQXvBjxziNMeo2oE1J0YCd0qs= -github.com/setavenger/blindbitd v0.0.0-20240602183715-c4e971bba3e4/go.mod h1:+L9bcVMbECVSqYjj/LhBKWNM4uVNRayN86EcVCrnFBI= github.com/setavenger/go-bip352 v0.1.7 h1:XckUqK+MadzOnfersbjXZuR9kLy40mW+BJGbWzYAt0A= github.com/setavenger/go-bip352 v0.1.7/go.mod h1:ajjkB64QrjbF0+MEUjeeBlBxDaJk7VmYUN8XbOK+EKo= github.com/setavenger/go-electrum v1.1.1 h1:2rowPjZE9BGLBHHenVwq6JWP0RKlqY2A5Bd6omprPjk= @@ -242,8 +242,8 @@ golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/config/auth.go b/internal/config/auth.go new file mode 100644 index 0000000..bf7e6e2 --- /dev/null +++ b/internal/config/auth.go @@ -0,0 +1,36 @@ +// internal/config/auth.go +package config + +import ( + "crypto/rand" + + "github.com/btcsuite/btcd/btcutil/base58" + petname "github.com/dustinkirkland/golang-petname" + "github.com/setavenger/blindbit-scan/pkg/types" +) + +var ( + authCredentials *types.AuthCredentials +) + +func GenerateAuthCredentials() *types.AuthCredentials { + username := petname.Generate(2, "-") + randomBytes := make([]byte, 16) + + rand.Read(randomBytes) // never returns an error + + password := base58.Encode(randomBytes) + + return &types.AuthCredentials{ + Username: username, + Password: password, + } +} + +func SetAuthCredentials(creds *types.AuthCredentials) { + authCredentials = creds +} + +func GetAuthCredentials() *types.AuthCredentials { + return authCredentials +} diff --git a/internal/config/load.go b/internal/config/load.go index 5d937d4..9800aad 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -3,7 +3,6 @@ package config import ( "encoding/hex" "log" - "log/slog" "strings" "time" @@ -27,8 +26,7 @@ func LoadConfigs(pathToConfig string) (err error) { // Handle errors reading the config file if err = viper.ReadInConfig(); err != nil { - logging.L.Err(err).Msg("Error reading config file") - return + logging.L.Warn().Err(err).Msg("Error reading config file") } // map ENV var names @@ -49,6 +47,7 @@ func LoadConfigs(pathToConfig string) (err error) { viper.BindEnv("auth.pass", "AUTH_PASS") viper.BindEnv("log_level", "LOG_LEVEL") + viper.BindEnv("privacy_mode", "PRIVACY_MODE") // app seed is for umbrel inputs // viper.BindEnv("external_app_seed", "EXTERNAL_APP_SEED") @@ -68,6 +67,7 @@ func LoadConfigs(pathToConfig string) (err error) { viper.SetDefault("wallet.birth_height", 840000) viper.SetDefault("log_level", "info") + viper.SetDefault("privacy_mode", false) // app seed // viper.SetDefault("external_app_seed", "") // we normally don't use it @@ -90,13 +90,15 @@ func LoadConfigs(pathToConfig string) (err error) { AutomaticScanInterval = 1 * time.Minute } + PrivateMode = viper.GetBool("privacy_mode") + // Basic Auth Data AuthUser = viper.GetString("auth.user") AuthPass = viper.GetString("auth.pass") - if AuthUser == "" || AuthPass == "" { - err := errors.New("config is missing auth settings") - slog.Error(err.Error()) + if (AuthUser == "" || AuthPass == "") && !PrivateMode { + err = errors.New("config is missing auth settings") + logging.L.Err(err).Msg("") return err } diff --git a/internal/config/paths.go b/internal/config/paths.go index 10bc881..832111e 100644 --- a/internal/config/paths.go +++ b/internal/config/paths.go @@ -1,7 +1,7 @@ package config // outsource to a common package as this is used across several blindbit programs -import "github.com/setavenger/blindbitd/src/utils" +import "github.com/setavenger/blindbit-scan/pkg/utils" var ( DirectoryPath = "~/.blindbit-scan" @@ -10,6 +10,7 @@ var ( PathConfig string PathDbWallet string PathDbNWC string + PathDbAuth string ) // needed for the flag default @@ -20,6 +21,7 @@ const PathEndingConfig = "/blindbit.toml" const PathEndingWallet = dataPath + "/wallet" const PathEndingNWC = dataPath + "/nwc" const PathEndingKeys = dataPath + "/keys" +const PathEndingBasicAuth = dataPath + "/basic_auth" func SetPaths(baseDirectory string) { if baseDirectory != "" { @@ -34,7 +36,7 @@ func SetPaths(baseDirectory string) { PathConfig = DirectoryPath + PathEndingConfig PathDbWallet = DirectoryPath + PathEndingWallet PathDbNWC = DirectoryPath + PathEndingNWC - + PathDbAuth = DirectoryPath + PathEndingBasicAuth // create the directories utils.TryCreateDirectoryPanic(DirectoryPath) diff --git a/internal/config/privatemode.go b/internal/config/privatemode.go new file mode 100644 index 0000000..7e929c9 --- /dev/null +++ b/internal/config/privatemode.go @@ -0,0 +1,13 @@ +package config + +var ( + UnlockChan = make(chan string) +) + +func init() { + UnlockChan = make(chan string) +} + +func Unlock() { + close(UnlockChan) +} diff --git a/internal/config/vars.go b/internal/config/vars.go index 3f5d413..3400a81 100644 --- a/internal/config/vars.go +++ b/internal/config/vars.go @@ -6,11 +6,24 @@ import ( "github.com/btcsuite/btcd/chaincfg" ) +type PrivateModeSetup struct { + ScanSecretKey [32]byte + SpendPubKey [33]byte + BirthHeight uint64 + LabelCount int + Password string + BasicAuthUser string + BasicAuthPass string +} + func init() { KeysReadyChan = make(chan struct{}) + PrivateModeSetupChan = make(chan PrivateModeSetup) } var ( + PrivateMode bool + // ExposeHttpHost if set gRPC will be exposed via http and not unix socket. This variable also defines the where it will be exposed. ExposeHttpHost string @@ -41,7 +54,9 @@ var ( SpendPubKey [33]byte - BirthHeight uint64 + // A reasonable default birth height + // Silent Payments was not used a lot before that + BirthHeight uint64 = 890000 LabelCount int @@ -52,4 +67,6 @@ var ( // keys are ready chan KeysReadyChan chan struct{} + + PrivateModeSetupChan chan PrivateModeSetup ) diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index ef555dc..d991455 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -9,6 +9,7 @@ import ( "github.com/setavenger/blindbit-scan/pkg/database" "github.com/setavenger/blindbit-scan/pkg/logging" "github.com/setavenger/blindbit-scan/pkg/networking" // todo move all blindbitd/src/* + "github.com/setavenger/blindbit-scan/pkg/types" "github.com/setavenger/blindbit-scan/pkg/wallet" "github.com/setavenger/go-electrum/electrum" ) @@ -22,38 +23,37 @@ type Daemon struct { Wallet *wallet.Wallet NewBlockChan <-chan *electrum.SubscribeHeadersResult TriggerRescanChan chan uint64 + AuthCredentials *types.AuthCredentials + DBWriter *database.DBWriter } // Will try to load a wallet from disk or will create a new one based on the blindbit.toml config-file -func SetupDaemon(path string) (*Daemon, error) { +func (d *Daemon) SetupExternalClients() (err error) { clientBlindBit := networking.ClientBlindBit{BaseUrl: config.BlindBitServerAddress} var clientElectrum *electrum.Client - var err error if config.UseElectrum { logging.L.Info().Msg("connecting to Electrum server") clientElectrum, err = networking.CreateElectrumClient(config.ElectrumServerAddress, config.ElectrumTorProxyHost) if err != nil { logging.L.Err(err).Msg("") - return nil, err + return err } } - w, err := database.TryLoadWalletFromDisk(path) - if err != nil { - logging.L.Err(err).Msg("") - return nil, err - } - d, err := NewDaemon(w, &clientBlindBit, clientElectrum) - if err != nil { - logging.L.Err(err).Msg("") - return nil, err - } + d.ClientBlindBit = &clientBlindBit + d.ClientElectrum = clientElectrum - return d, err + return nil } -func NewDaemon(wallet *wallet.Wallet, clientBlindBit *networking.ClientBlindBit, clientElectrum *electrum.Client) (*Daemon, error) { +func NewDaemon( + wallet *wallet.Wallet, + clientBlindBit *networking.ClientBlindBit, + clientElectrum *electrum.Client, +) ( + *Daemon, error, +) { var channel <-chan *electrum.SubscribeHeadersResult var err error if config.UseElectrum { @@ -106,6 +106,10 @@ func SetupDaemonNoWallet() (*Daemon, error) { return d, err } +func (d *Daemon) SetDbWriter(dbWriter *database.DBWriter) { + d.DBWriter = dbWriter +} + // ResetDaemonAndWallet deletes the stored wallet DB // used when new keys are added such that scanning continues from scratch func (d *Daemon) ResetDaemonAndWallet() (err error) { @@ -128,5 +132,23 @@ func (d *Daemon) Cancel() { } func (d *Daemon) SaveWalletToDB() (err error) { - return database.WriteWalletToDB(config.PathDbWallet, d.Wallet) + if d.Wallet == nil { + return nil + } + return d.DBWriter.WriteWalletToDB(config.PathDbWallet, d.Wallet) +} + +func (d *Daemon) LoadAuthCredentials() error { + var creds types.AuthCredentials + if err := d.DBWriter.ReadFromDB(config.PathDbAuth, &creds); err != nil { + return err + } + d.AuthCredentials = &creds + config.SetAuthCredentials(&creds) + return nil +} + +func (d *Daemon) Unlock(password string) error { + d.DBWriter = &database.DBWriter{Password: password} + return d.LoadAuthCredentials() } diff --git a/internal/daemon/scan.go b/internal/daemon/scan.go index fa33bda..bd3339a 100644 --- a/internal/daemon/scan.go +++ b/internal/daemon/scan.go @@ -13,7 +13,6 @@ import ( "github.com/btcsuite/btcd/btcutil/gcs/builder" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/setavenger/blindbit-scan/internal/config" - "github.com/setavenger/blindbit-scan/pkg/database" "github.com/setavenger/blindbit-scan/pkg/logging" "github.com/setavenger/blindbit-scan/pkg/networking" "github.com/setavenger/blindbit-scan/pkg/utils" // todo move blindbitd/src to a pkg for all blindbit programs @@ -255,7 +254,7 @@ func (d *Daemon) SyncToTip(chainTip uint64) error { if i%100 == 0 { // do some writes anyways to save the last state of the scan height - err = database.WriteWalletToDB(config.PathDbWallet, d.Wallet) + err = d.DBWriter.WriteWalletToDB(config.PathDbWallet, d.Wallet) if err != nil { logging.L.Err(err).Uint64("height", i).Msg("") return err @@ -276,7 +275,7 @@ func (d *Daemon) SyncToTip(chainTip uint64) error { } // todo: database should be an interface to allow other forms of storing data. - err = database.WriteWalletToDB(config.PathDbWallet, d.Wallet) + err = d.DBWriter.WriteWalletToDB(config.PathDbWallet, d.Wallet) if err != nil { logging.L.Err(err).Msg("") return err @@ -493,7 +492,7 @@ func (d *Daemon) ForceSyncFrom(fromHeight uint64) error { } if i%100 == 0 { // do some writes anyways to save the last state of the scan height - err = database.WriteWalletToDB(config.PathDbWallet, d.Wallet) + err = d.DBWriter.WriteWalletToDB(config.PathDbWallet, d.Wallet) if err != nil { logging.L.Err(err).Msg("") return err @@ -511,7 +510,7 @@ func (d *Daemon) ForceSyncFrom(fromHeight uint64) error { // todo: make more robust, unnecessary trick d.Wallet.LastScanHeight = i } - err = database.WriteWalletToDB(config.PathDbWallet, d.Wallet) + err = d.DBWriter.WriteWalletToDB(config.PathDbWallet, d.Wallet) if err != nil { logging.L.Err(err).Msg("") return err diff --git a/internal/server/middleware.go b/internal/server/middleware.go new file mode 100644 index 0000000..6b9da00 --- /dev/null +++ b/internal/server/middleware.go @@ -0,0 +1,28 @@ +package server + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/setavenger/blindbit-scan/internal/config" +) + +// internal/server/middleware.go +func (s *Server) basicAuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + creds := config.GetAuthCredentials() + if creds == nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Wallet not unlocked"}) + c.Abort() + return + } + + username, password, ok := c.Request.BasicAuth() + if !ok || username != creds.Username || password != creds.Password { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) + c.Abort() + return + } + c.Next() + } +} diff --git a/internal/server/routes.go b/internal/server/routes.go index 312399d..a37e159 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -1,11 +1,9 @@ package server import ( - "bytes" "encoding/hex" "fmt" "net/http" - "time" "github.com/gin-gonic/gin" "github.com/setavenger/blindbit-scan/internal/config" @@ -63,11 +61,12 @@ func (s *Server) PostRescan(c *gin.Context) { } type SetupReq struct { - ScanSecret string `json:"secret_sec"` + ScanSecret string `json:"scan_secret"` SpendPublic string `json:"spend_pub"` BirthHeight uint `json:"birth_height"` } +// todo: fix block after calling while sync is running func (s *Server) PutSilentPaymentKeys(c *gin.Context) { var err error @@ -88,7 +87,11 @@ func (s *Server) PutSilentPaymentKeys(c *gin.Context) { c.Abort() return } - viper.Set("wallet.scan_secret_key", keys.ScanSecret) + if len(scanSecret) != 32 { + c.JSON(http.StatusInternalServerError, gin.H{"err": "scan secret must be 32 bytes"}) + c.Abort() + return + } spendPub, err := hex.DecodeString(keys.SpendPublic) if err != nil { @@ -96,13 +99,21 @@ func (s *Server) PutSilentPaymentKeys(c *gin.Context) { c.Abort() return } - viper.Set("wallet.spend_pub_key", keys.SpendPublic) - viper.Set("wallet.birth_height", keys.BirthHeight) + if len(spendPub) != 33 { + c.JSON(http.StatusInternalServerError, gin.H{"err": "spend public key must be 33 bytes"}) + c.Abort() + return + } // we only write if nothing before failed if keys.BirthHeight < 1 { keys.BirthHeight = 1 } + + viper.Set("wallet.scan_secret_key", keys.ScanSecret) + viper.Set("wallet.spend_pub_key", keys.SpendPublic) + viper.Set("wallet.birth_height", keys.BirthHeight) + config.BirthHeight = uint64(keys.BirthHeight) config.ScanSecretKey = bip352.ConvertToFixedLength32(scanSecret) config.SpendPubKey = bip352.ConvertToFixedLength33(spendPub) @@ -116,17 +127,16 @@ func (s *Server) PutSilentPaymentKeys(c *gin.Context) { return } - go func() { - if s.Daemon.Wallet == nil || bytes.Equal(s.Daemon.Wallet.SecretKeyScan[:], make([]byte, 32)) || bytes.Equal(s.Daemon.Wallet.PubKeySpend[:], make([]byte, 33)) { - config.KeysReadyChan <- struct{}{} - } - }() - var newWallet *wallet.Wallet // logging.L.Trace().Any("birth", config.BirthHeight).Any("l-count", config.LabelCount).Any("scan", config.ScanSecretKey).Any("spend", config.SpendPubKey).Msg("config info") - newWallet, err = wallet.SetupWallet(config.BirthHeight, config.LabelCount, config.ScanSecretKey, config.SpendPubKey) + newWallet, err = wallet.SetupWallet( + config.BirthHeight, + config.LabelCount, + config.ScanSecretKey, + config.SpendPubKey, + ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"err": err.Error()}) c.Abort() @@ -143,25 +153,16 @@ func (s *Server) PutSilentPaymentKeys(c *gin.Context) { s.Daemon.Wallet = newWallet - // logging.L.Debug().Any("wallet", s.Daemon.Wallet).Msg("") - - go func() { - <-time.After(5 * time.Second) - err = s.Daemon.ContinuousScan() - if err != nil { - logging.L.Err(err).Msg("") - return - } - }() - - // logging.L.Trace().Any("wallet", s.Daemon.Wallet).Msg("") - address, err := s.Daemon.Wallet.GenerateAddress() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"err": err.Error()}) c.Abort() return } + + // no routine to alert the caller if something is wrong + config.KeysReadyChan <- struct{}{} + c.JSON(http.StatusOK, gin.H{"address": address}) } @@ -173,7 +174,7 @@ func (s *Server) NewNwcConnection(c *gin.Context) { return } - err = database.WriteNip47ControllerToDB(config.PathDbNWC, s.Nip47Controller) + err = s.Daemon.DBWriter.WriteNip47ControllerToDB(config.PathDbNWC, s.Nip47Controller) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"err": err.Error()}) c.Abort() @@ -182,3 +183,123 @@ func (s *Server) NewNwcConnection(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"uri": nwcURI}) } + +// UnlockReq represents the request to unlock the wallet +type UnlockReq struct { + Password string `json:"password"` +} + +// UnlockResponse represents the response from unlocking the wallet +type UnlockResponse struct { + Success bool `json:"success"` + Error string `json:"error,omitempty"` +} + +func (s *Server) Unlock(c *gin.Context) { + var req UnlockReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) + return + } + + // todo: several instances of password being passed around + // todo: this should be refactored + dbWriter := database.DBWriter{ + Password: req.Password, + } + + s.Daemon.SetDbWriter(&dbWriter) + + // Decrypt wallet data + if err := s.Daemon.Unlock(req.Password); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to unlock wallet"}) + return + } + + // Load auth credentials + if err := s.Daemon.LoadAuthCredentials(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load auth credentials"}) + return + } + + config.UnlockChan <- req.Password + + c.JSON(http.StatusOK, UnlockResponse{Success: true}) +} + +type SetupInstanceReq struct { + ScanSecret string `json:"scan_secret"` + SpendPublic string `json:"spend_pub"` + BirthHeight uint `json:"birth_height"` + Password string `json:"password"` +} + +// SetupInstance is used to setup the instance for the first time +// it will generate a new set of auth credentials and save them to the database +// the keys and the unlock password have to be sent in the request body +func (s *Server) SetupInstance(c *gin.Context) { + var req SetupInstanceReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) + return + } + + // load keys + scanSecret, err := hex.DecodeString(req.ScanSecret) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"err": err.Error()}) + c.Abort() + return + } + if len(scanSecret) != 32 { + c.JSON(http.StatusInternalServerError, gin.H{"err": "scan secret must be 32 bytes"}) + c.Abort() + return + } + + spendPub, err := hex.DecodeString(req.SpendPublic) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"err": err.Error()}) + c.Abort() + return + } + if len(spendPub) != 33 { + c.JSON(http.StatusInternalServerError, gin.H{"err": "spend public key must be 33 bytes"}) + c.Abort() + return + } + + setup := config.PrivateModeSetup{ + ScanSecretKey: [32]byte(scanSecret), + SpendPubKey: [33]byte(spendPub), + BirthHeight: uint64(req.BirthHeight), + Password: req.Password, + } + + config.PrivateModeSetupChan <- setup + + creds := config.GenerateAuthCredentials() + config.SetAuthCredentials(creds) + // Wait for auth credentials to be generated and loaded + if creds == nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate auth credentials"}) + return + } + + tempWriter := database.DBWriter{ + Password: req.Password, + } + + // Save auth credentials to disk + if err := tempWriter.SaveAuthCredentials(creds); err != nil { + logging.L.Error().Err(err).Msg("Failed to save auth credentials") + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save auth credentials"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "username": creds.Username, + "password": creds.Password, + }) +} diff --git a/internal/server/server.go b/internal/server/server.go index b670e75..b30d5e8 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,14 +1,15 @@ package server import ( - "log/slog" "net/http" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/setavenger/blindbit-scan/internal/config" "github.com/setavenger/blindbit-scan/internal/daemon" + "github.com/setavenger/blindbit-scan/pkg/logging" "github.com/setavenger/blindbit-scan/pkg/networking/nwc" + "github.com/setavenger/blindbit-scan/pkg/utils" ) func StartNewServer( @@ -40,11 +41,34 @@ func (s *Server) RunServer() error { AllowCredentials: true, })) - router.Use(gin.BasicAuth(gin.Accounts{ - config.AuthUser: config.AuthPass, - })) + // we only allow this in simple mode + // too much complexity in private mode + if !config.PrivateMode { + router.PUT("/new-keys", s.PutSilentPaymentKeys) + } + + // setup instance no auth + if config.PrivateMode { + // setup instance + router.POST("/setup-instance", func(ctx *gin.Context) { + authExists := utils.CheckIfFileExists(config.PathDbAuth) + walletExists := utils.CheckIfFileExists(config.PathDbWallet) + + if authExists || walletExists { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "instance already (partially) setup"}) + ctx.Abort() + return + } + ctx.Next() + }, s.SetupInstance) + + // unlock the wallet + // this is used to unlock the wallet when the user has already setup the instance + // no basic auth because basic auth is decrypted with the password + router.POST("/unlock", s.Unlock) + } - router.PUT("/new-keys", s.PutSilentPaymentKeys) + router.Use(s.basicAuthMiddleware()) // BlindBit adaptation of Nostr Wallet Connect router.POST("/new-nwc-connection", s.NewNwcConnection) @@ -67,7 +91,7 @@ func (s *Server) RunServer() error { walletReadyGroup.POST("/rescan", s.PostRescan) if err := router.Run(config.ExposeHttpHost); err != nil { - slog.Error(err.Error()) + logging.L.Err(err).Msg("failed to start server") return err } return nil diff --git a/internal/startup/base.go b/internal/startup/base.go new file mode 100644 index 0000000..a01410c --- /dev/null +++ b/internal/startup/base.go @@ -0,0 +1,97 @@ +package startup + +import ( + "flag" + "os" + "os/signal" + + "github.com/setavenger/blindbit-scan/internal/config" + "github.com/setavenger/blindbit-scan/internal/daemon" + "github.com/setavenger/blindbit-scan/internal/server" + "github.com/setavenger/blindbit-scan/pkg/logging" + "github.com/setavenger/blindbit-scan/pkg/networking/nwc" +) + +func init() { + // todo can this double reference work? + flag.StringVar( + &config.DirectoryPath, + "datadir", + config.DefaultDirectoryPath, + "Set the base directory for blindbit-scan. Default directory is ~/.blindbit-scan.", + ) + + flag.BoolVar(&config.PrivateMode, "private", false, "BlindBit Scan will run in private mode. All data on disk will be encrypted all data will only be decrypted in memory. Upon restart the unlock endpoint needs to be called to decrypt data and start the scanning.") + + flag.Parse() +} + +func RunProgram() { + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + var err error + + err = config.SetupConfigs(config.DirectoryPath) + if err != nil { + logging.L.Panic().Err(err). + Msg("startup failed, could not setup configs") + } + + var d *daemon.Daemon + d, err = daemon.NewDaemon(nil, nil, nil) + if err != nil { + logging.L.Panic().Err(err). + Msg("startup failed, could not setup daemon") + } + + var controller = &nwc.Nip47Controller{} + + // http server + go func() { + err = server.StartNewServer(d, controller) + if err != nil { + logging.L.Panic().Err(err). + Msg("startup failed, could start server") + } + }() + + // when we exit we still flush the last state + defer func() { + if d != nil { + d.SaveWalletToDB() + } + }() + + go func() { + var d2 *daemon.Daemon + if config.PrivateMode { + logging.L.Info().Msg("running in privacy mode") + logging.L.Info().Msg("privacy mode requires a setup api call to setup the instance") + d2, err = StartupWithPrivateMode(controller) + } else { + d2, err = StartupWithSimpleMode() + } + + if err != nil { + logging.L.Panic().Err(err). + Msg("startup failed, could not setup daemon") + } + + *d = *d2 + + // if the wallet is not setup we wait for the keys ready signal + if d.Wallet == nil { + logging.L.Info().Msg("waiting for keys") + <-config.KeysReadyChan + err = d.SetupExternalClients() + if err != nil { + logging.L.Panic().Err(err). + Msg("startup failed, could setup full daemon") + } + } + go d.ContinuousScan() + }() + + // wait for program stop signal + <-interrupt +} diff --git a/internal/startup/private.go b/internal/startup/private.go new file mode 100644 index 0000000..19f312e --- /dev/null +++ b/internal/startup/private.go @@ -0,0 +1,107 @@ +package startup + +import ( + "context" + + "github.com/setavenger/blindbit-scan/internal" + "github.com/setavenger/blindbit-scan/internal/config" + "github.com/setavenger/blindbit-scan/internal/daemon" + nwcserver "github.com/setavenger/blindbit-scan/internal/nwc_server" + "github.com/setavenger/blindbit-scan/pkg/database" + "github.com/setavenger/blindbit-scan/pkg/logging" + "github.com/setavenger/blindbit-scan/pkg/networking/nwc" + "github.com/setavenger/blindbit-scan/pkg/wallet" +) + +func StartupWithPrivateMode(nip47Controller *nwc.Nip47Controller) (d *daemon.Daemon, err error) { + walletExists := internal.CheckIfFileExists(config.PathDbWallet) + authExists := internal.CheckIfFileExists(config.PathDbAuth) + + if walletExists && authExists { + // we need to load the existing instance, wait for unlock api call + logging.L.Info().Msg("Waiting for unlock request") + d, err = SetupExistingInstancePrivateMode() + if err != nil { + logging.L.Panic().Err(err). + Msg("startup failed, could produce daemon hull") + } + } else { + // we need to setup a whole new instance and wait for Setup rest api call + logging.L.Info().Msg("Waiting for setup-instance request") + d, err = SetupNewInstancePrivateMode() + if err != nil { + logging.L.Panic().Err(err). + Msg("startup failed, could produce daemon hull") + } + } + + // Setup BlindBit Nostr Wallet Connect + nwcServer := nwcserver.NewNwcServer(d) + + var controller *nwc.Nip47Controller + logging.L.Info().Msg("attempting to load NWC apps from disk") + controller, err = d.DBWriter.TryLoadingControllerFromDisk(context.Background(), config.PathDbNWC) + if err != nil { + logging.L.Panic().Err(err).Msg("failed to create new controller") + } + logging.L.Trace().Any("apps", controller.Apps()).Msg("controller data") + + controller.RegisterHandler(nwc.GET_INFO_METHOD, nwcServer.GetInfoHandler()) + controller.RegisterHandler(nwc.GET_BALANCE_METHOD, nwcServer.GetBalanceHandler()) + controller.RegisterHandler(nwc.LIST_UTXOS_METHOD, nwcServer.ListUtxosHandler()) + + err = controller.ConnectRelay() + if err != nil { + logging.L.Panic().Err(err).Msg("failed to connect to relay") + } + go controller.StartListening() + + *nip47Controller = *controller + + return d, nil +} + +func SetupExistingInstancePrivateMode() (d *daemon.Daemon, err error) { + password := <-config.UnlockChan + d, err = daemon.SetupDaemonNoWallet() + if err != nil { + logging.L.Panic().Err(err). + Msg("startup failed, could only produce daemon hull") + } + + d.SetDbWriter(&database.DBWriter{Password: password}) + + w, err := d.DBWriter.LoadWalletFromDisk(config.PathDbWallet) + if err != nil { + logging.L.Warn().Err(err). + Msg("startup failed, could setup full daemon") + } + d.Wallet = w + + return d, nil +} + +func SetupNewInstancePrivateMode() (d *daemon.Daemon, err error) { + setup := <-config.PrivateModeSetupChan + d, err = daemon.SetupDaemonNoWallet() + if err != nil { + logging.L.Panic().Err(err). + Msg("startup failed, could only produce daemon hull") + } + + if setup.LabelCount == 0 { + setup.LabelCount = 5 + } + + w, err := wallet.SetupWallet(setup.BirthHeight, setup.LabelCount, setup.ScanSecretKey, setup.SpendPubKey) + if err != nil { + logging.L.Err(err). + Msg("startup failed, could not setup wallet") + return nil, err + } + d.Wallet = w + + d.SetDbWriter(&database.DBWriter{Password: setup.Password}) + + return d, nil +} diff --git a/internal/startup/simple.go b/internal/startup/simple.go new file mode 100644 index 0000000..4ea391d --- /dev/null +++ b/internal/startup/simple.go @@ -0,0 +1,67 @@ +package startup + +import ( + "github.com/setavenger/blindbit-scan/internal" + "github.com/setavenger/blindbit-scan/internal/config" + "github.com/setavenger/blindbit-scan/internal/daemon" + "github.com/setavenger/blindbit-scan/pkg/database" + "github.com/setavenger/blindbit-scan/pkg/logging" + "github.com/setavenger/blindbit-scan/pkg/wallet" +) + +func StartupWithSimpleMode() (d *daemon.Daemon, err error) { + if internal.CheckIfFileExists(config.PathDbWallet) { + d, err = SetupExistingInstanceSimpleMode() + } else { + d, err = SetupNewInstanceSimpleMode() + } + if err != nil { + logging.L.Panic().Err(err). + Msg("startup failed, could only produce daemon hull") + } + + return d, nil +} + +func SetupNewInstanceSimpleMode() (d *daemon.Daemon, err error) { + // no password needed, so we just do the old process + d, err = daemon.SetupDaemonNoWallet() + if err != nil { + logging.L.Panic().Err(err). + Msg("startup failed, could only produce daemon hull") + } + + // no password needed, so we just set it to empty string + d.SetDbWriter(&database.DBWriter{Password: ""}) + + if config.ScanSecretKey != [32]byte{} && config.SpendPubKey != [33]byte{} { + w, err := wallet.SetupWallet(config.BirthHeight, config.LabelCount, config.ScanSecretKey, config.SpendPubKey) + if err != nil { + logging.L.Panic().Err(err). + Msg("startup failed, could not setup wallet") + } + d.Wallet = w + } + return d, nil +} + +func SetupExistingInstanceSimpleMode() (d *daemon.Daemon, err error) { + // no password needed, so we just do the old process + d, err = daemon.SetupDaemonNoWallet() + if err != nil { + logging.L.Panic().Err(err). + Msg("startup failed, could only produce daemon hull") + } + + // no password needed, so we just set it to empty string + d.SetDbWriter(&database.DBWriter{Password: ""}) + + w, err := d.DBWriter.LoadWalletFromDisk(config.PathDbWallet) + if err != nil { + logging.L.Warn().Err(err). + Msg("startup failed, could setup full daemon") + } + d.Wallet = w + + return d, nil +} diff --git a/pkg/database/db.go b/pkg/database/db.go index e46dd72..ce4efe0 100644 --- a/pkg/database/db.go +++ b/pkg/database/db.go @@ -2,12 +2,20 @@ package database import ( "context" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "errors" + "fmt" + "io" "os" "github.com/setavenger/blindbit-scan/internal" "github.com/setavenger/blindbit-scan/internal/config" "github.com/setavenger/blindbit-scan/pkg/logging" "github.com/setavenger/blindbit-scan/pkg/networking/nwc" + "github.com/setavenger/blindbit-scan/pkg/types" "github.com/setavenger/blindbit-scan/pkg/wallet" ) @@ -16,6 +24,115 @@ type Serialiser interface { DeSerialise([]byte) error } +type DBWriter struct { + Password string +} + +func (w *DBWriter) WriteToDB(path string, dataStruct Serialiser) error { + if config.PrivateMode { + data, err := dataStruct.Serialise() + if err != nil { + logging.L.Err(err).Msg("failed to serialise data") + return err + } + data, err = EncryptData(data, w.Password) + if err != nil { + logging.L.Err(err).Msg("failed to encrypt data") + return err + } + err = os.WriteFile(path, data, 0644) + if err != nil { + logging.L.Err(err).Msg("failed to write to db") + return err + } + return nil + } + return WriteToDB(path, dataStruct) +} + +func (w *DBWriter) ReadFromDB(path string, dataStruct Serialiser) error { + if config.PrivateMode { + data, err := os.ReadFile(path) + if err != nil { + logging.L.Err(err).Msg("failed to read from db") + return err + } + data, err = DecryptData(data, w.Password) + if err != nil { + logging.L.Err(err).Msg("failed to decrypt data") + return err + } + return dataStruct.DeSerialise(data) + } + return ReadFromDB(path, dataStruct) +} + +func (w *DBWriter) EncryptData(data []byte) ([]byte, error) { + return EncryptData(data, w.Password) +} + +// AES GCM encryption function +func EncryptData(data []byte, password string) ([]byte, error) { + // Hash the password to get a 32-byte key + key := sha256.Sum256([]byte(password)) + + // Create a new AES cipher block using the hashed key + block, err := aes.NewCipher(key[:]) + if err != nil { + return nil, err + } + + // Create a new GCM cipher mode + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + // Generate a random nonce + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + // Encrypt the data using GCM + ciphertext := gcm.Seal(nonce, nonce, data, nil) + return ciphertext, nil +} + +func DecryptData(data []byte, password string) ([]byte, error) { + // Hash the password to get a 32-byte key + key := sha256.Sum256([]byte(password)) + + // Create a new AES cipher block using the hashed key + block, err := aes.NewCipher(key[:]) + if err != nil { + return nil, err + } + + // Create a new GCM cipher mode + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + // Get the nonce size + nonceSize := gcm.NonceSize() + if len(data) < nonceSize { + return nil, fmt.Errorf("ciphertext too short") + } + + // Extract nonce from ciphertext + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + + // Decrypt the data + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, err + } + + return plaintext, nil +} + func WriteToDB(path string, dataStruct Serialiser) error { data, err := dataStruct.Serialise() if err != nil { @@ -47,37 +164,32 @@ func ReadFromDB(path string, dataStruct Serialiser) error { return nil } -func WriteWalletToDB(p string, w *wallet.Wallet) error { - if w == nil { - // do nothting +func (w *DBWriter) WriteWalletToDB(p string, wallet *wallet.Wallet) error { + if wallet == nil { + // do nothing logging.L.Warn().Msg("wallet was nil") return nil } - return WriteToDB(p, w) + return w.WriteToDB(p, wallet) } -func WriteNip47ControllerToDB(p string, c *nwc.Nip47Controller) error { - return WriteToDB(p, c) +func (w *DBWriter) WriteNip47ControllerToDB(p string, c *nwc.Nip47Controller) error { + return w.WriteToDB(p, c) } -func TryLoadWalletFromDisk(path string) (*wallet.Wallet, error) { +func (w *DBWriter) LoadWalletFromDisk(path string) (*wallet.Wallet, error) { if internal.CheckIfFileExists(path) { - var w wallet.Wallet - err := ReadFromDB(path, &w) - return &w, err + var wallet wallet.Wallet + err := w.ReadFromDB(path, &wallet) + return &wallet, err } logging.L.Trace().Str("path", path).Msg("No wallet data on disk") - return wallet.SetupWallet( - config.BirthHeight, - config.LabelCount, - config.ScanSecretKey, - config.SpendPubKey, - ) + return nil, errors.New("no wallet data on disk") } -func TryLoadingControllerFromDisk( +func (w *DBWriter) TryLoadingControllerFromDisk( ctx context.Context, path string, ) ( @@ -86,7 +198,7 @@ func TryLoadingControllerFromDisk( ) { if internal.CheckIfFileExists(path) { var apps nwc.Apps - err = ReadFromDB(path, &apps) + err = w.ReadFromDB(path, &apps) if err != nil { logging.L.Err(err).Msg("failed to load apps from db") return nil, err @@ -99,3 +211,13 @@ func TryLoadingControllerFromDisk( return nwc.NewNip47Controller(ctx), nil } + +func (w *DBWriter) SaveAuthCredentials(creds *types.AuthCredentials) error { + return w.WriteToDB(config.PathDbAuth, creds) +} + +func (w *DBWriter) LoadAuthCredentials() (*types.AuthCredentials, error) { + var creds types.AuthCredentials + err := w.ReadFromDB(config.PathDbAuth, &creds) + return &creds, err +} diff --git a/pkg/networking/blindbit.go b/pkg/networking/blindbit.go index 043df21..e698a1a 100644 --- a/pkg/networking/blindbit.go +++ b/pkg/networking/blindbit.go @@ -9,7 +9,7 @@ import ( "net/http" "github.com/setavenger/blindbit-scan/pkg/logging" - "github.com/setavenger/blindbitd/src/utils" + "github.com/setavenger/blindbit-scan/pkg/utils" "github.com/setavenger/go-bip352" ) diff --git a/pkg/networking/nwc.go b/pkg/networking/nwc.go index a5113ad..235133b 100644 --- a/pkg/networking/nwc.go +++ b/pkg/networking/nwc.go @@ -33,13 +33,6 @@ type GetBalanceResponse struct { Balance int64 `json:"balance"` // in millisatoshis } -const ( - privKeyWalletService = "66d1069f89c77d846cca006d132f2493a0acd31f650e000bc5199306556d8b21" - pubKeyWalletService = "3e6221b76a6555c819c8d0c87d9af47ce1b31c0b413f276b2f0d8ddd2a3e1c1f" - privKeyClientSecret = "125b81e4b8b21c889374ebf1a4b786791f0088a4f3b64260e75fd759497c2700" - pubKeyClient = "e89262ebbe2b114bfa1f53b0b65a46a9bdf65ac2c11a7969933c9aac8a4e83dd" -) - // publishInfoEvent publishes a replaceable info event (kind 13194) to the relay. func publishInfoEvent(ctx context.Context, relay *nostr.Relay, privKey, pubKey string) { // Supported commands as a space-separated string. diff --git a/pkg/networking/nwc/nip47_controller.go b/pkg/networking/nwc/nip47_controller.go index e7adedb..590c395 100644 --- a/pkg/networking/nwc/nip47_controller.go +++ b/pkg/networking/nwc/nip47_controller.go @@ -167,6 +167,9 @@ func (c *Nip47Controller) StartListening() { for { select { case ev := <-sub.Events: + if ev == nil { + continue + } logging.L.Info().Str("event-id", ev.ID).Msg("received event") go c.processEvent(ev) case <-c.ctx.Done(): diff --git a/pkg/types/auth.go b/pkg/types/auth.go new file mode 100644 index 0000000..b3836d0 --- /dev/null +++ b/pkg/types/auth.go @@ -0,0 +1,16 @@ +package types + +import "encoding/json" + +type AuthCredentials struct { + Username string `json:"username"` + Password string `json:"password"` +} + +func (a *AuthCredentials) Serialise() ([]byte, error) { + return json.Marshal(a) +} + +func (a *AuthCredentials) DeSerialise(data []byte) error { + return json.Unmarshal(data, a) +} diff --git a/pkg/utils/bytes.go b/pkg/utils/bytes.go new file mode 100644 index 0000000..3b85ad6 --- /dev/null +++ b/pkg/utils/bytes.go @@ -0,0 +1,8 @@ +package utils + +// ConvertToFixedLength34 converts a byte slice to a fixed length array of 34 bytes +func ConvertToFixedLength34(data []byte) [34]byte { + var result [34]byte + copy(result[:], data) + return result +} diff --git a/pkg/utils/path.go b/pkg/utils/path.go new file mode 100644 index 0000000..3ceb7f5 --- /dev/null +++ b/pkg/utils/path.go @@ -0,0 +1,35 @@ +package utils + +import ( + "os" + "path/filepath" + "strings" + + "github.com/setavenger/blindbit-scan/pkg/logging" +) + +// ResolvePath resolves a path, expanding ~ to the user's home directory +func ResolvePath(path string) string { + if strings.HasPrefix(path, "~") { + home, err := os.UserHomeDir() + if err != nil { + logging.L.Panic().Err(err).Msg("failed to get user home directory") + } + path = filepath.Join(home, path[1:]) + } + return path +} + +// CheckIfFileExists checks if a file exists +func CheckIfFileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +// TryCreateDirectoryPanic creates a directory and panics if it fails +func TryCreateDirectoryPanic(path string) { + err := os.MkdirAll(path, 0755) + if err != nil { + logging.L.Panic().Err(err).Msg("failed to create directory") + } +}