From 1238888d159f84f8a58dd47bdcac66c0288fef2b Mon Sep 17 00:00:00 2001 From: Maphikza Date: Thu, 10 Oct 2024 11:43:24 +0200 Subject: [PATCH 01/21] Updating relay settings type to allow for filtering through relay settings for leaf file types. --- lib/handlers/scionic/utils.go | 73 +++++++++++++++++++++++++++++++++++ lib/types.go | 5 +++ services/server/port/main.go | 15 ++++++- 3 files changed, 92 insertions(+), 1 deletion(-) diff --git a/lib/handlers/scionic/utils.go b/lib/handlers/scionic/utils.go index 60d949f..182432f 100644 --- a/lib/handlers/scionic/utils.go +++ b/lib/handlers/scionic/utils.go @@ -3,11 +3,15 @@ package scionic import ( "fmt" "io" + "log" + "path/filepath" "slices" "strconv" + "strings" "time" "github.com/fxamacker/cbor/v2" + "github.com/spf13/viper" merkle_dag "github.com/HORNET-Storage/scionic-merkletree/dag" @@ -134,3 +138,72 @@ func WriteMessageToStream[T any](stream types.Stream, message T) error { return nil } + +// Function to check file permission based on RelaySettings, loading settings internally +func IsFilePermitted(filename string) bool { + // Load relay settings + settings, err := LoadRelaySettings() + if err != nil { + log.Fatalf("Failed to load relay settings: %v", err) + return false + } + + // Extract the file extension and make it lowercase for case-insensitive comparison + fileExtension := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), ".")) + + // Check mode setting + if settings.Mode == "smart" { + // Smart mode: Check if the file extension is explicitly allowed + + if settings.IsPhotosActive && contains(settings.PhotoTypes, fileExtension) { + return true + } + if settings.IsVideosActive && contains(settings.VideoTypes, fileExtension) { + return true + } + if settings.IsAudioActive && contains(settings.AudioTypes, fileExtension) { + return true + } + + return false // File type is not permitted in "smart" mode if it doesn't match any active type + } else if settings.Mode == "unlimited" { + // Unlimited mode: Allow everything except explicitly blocked types + + if contains(settings.Photos, fileExtension) || contains(settings.Videos, fileExtension) || contains(settings.Audio, fileExtension) { + return false // File type is explicitly blocked in "unlimited" mode + } + + return true // File type is permitted in "unlimited" mode + } + + return false // Default to false if the mode is not recognized +} + +// Helper function to check if a slice contains a given string +func contains(list []string, item string) bool { + for _, element := range list { + if element == item { + return true + } + } + return false +} + +func LoadRelaySettings() (*types.RelaySettings, error) { + viper.SetConfigName("config") // Name of config file (without extension) + viper.SetConfigType("json") // Type of the config file + viper.AddConfigPath(".") // Path to look for the config file in + + if err := viper.ReadInConfig(); err != nil { + log.Fatalf("Error reading config file: %s", err) + return nil, err + } + + var settings types.RelaySettings + if err := viper.UnmarshalKey("relay_settings", &settings); err != nil { + log.Fatalf("Error unmarshaling config into struct: %s (nostr/utils)", err) + return nil, err + } + + return &settings, nil +} diff --git a/lib/types.go b/lib/types.go index b092bad..fee1900 100644 --- a/lib/types.go +++ b/lib/types.go @@ -186,6 +186,11 @@ type RelaySettings struct { IsVideosActive bool `json:"isVideosActive"` IsGitNestrActive bool `json:"isGitNestrActive"` IsAudioActive bool `json:"isAudioActive"` + + // New fields for the file type lists + PhotoTypes []string `json:"photoTypes"` + VideoTypes []string `json:"videoTypes"` + AudioTypes []string `json:"audioTypes"` } type TimeSeriesData struct { diff --git a/services/server/port/main.go b/services/server/port/main.go index ef0c0cb..6f6e64a 100644 --- a/services/server/port/main.go +++ b/services/server/port/main.go @@ -102,8 +102,21 @@ func init() { "Audio": []string{}, "Protocol": []string{}, // Default empty Protocol and Chunked lists "Chunked": []string{}, - }) + // New default file type lists for Photos, Videos, and Audio + "PhotoFileTypes": []string{ + "jpeg", "jpg", "png", "gif", "bmp", "tiff", "raw", "svg", + "eps", "psd", "ai", "pdf", "webp", + }, + "VideoFileTypes": []string{ + "avi", "mp4", "mov", "wmv", "mkv", "flv", "mpeg", + "3gp", "webm", "ogg", + }, + "AudioFileTypes": []string{ + "mp3", "wav", "ogg", "flac", "aac", "wma", "m4a", + "opus", "m4b", "midi", "mp4", "webm", "3gp", + }, + }) // Generate a random wallet API key apiKey, err := generateRandomAPIKey() if err != nil { From 75f13c4752e489608a86440d26f7641cd8360263 Mon Sep 17 00:00:00 2001 From: Maphikza Date: Thu, 10 Oct 2024 11:57:34 +0200 Subject: [PATCH 02/21] Improving IsFilePermitted to handle miscellanious files as we account for these in our relay panel stats. --- lib/handlers/scionic/utils.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/handlers/scionic/utils.go b/lib/handlers/scionic/utils.go index 182432f..1625128 100644 --- a/lib/handlers/scionic/utils.go +++ b/lib/handlers/scionic/utils.go @@ -165,7 +165,14 @@ func IsFilePermitted(filename string) bool { return true } - return false // File type is not permitted in "smart" mode if it doesn't match any active type + // Miscellaneous case: If the file type is not in the known lists but also not blocked, allow it + if !contains(settings.Photos, fileExtension) && + !contains(settings.Videos, fileExtension) && + !contains(settings.Audio, fileExtension) { + return true // Permit miscellaneous files in smart mode if they are not explicitly blocked + } + + return false // File type is not permitted in "smart" mode if it doesn't match any active or miscellaneous type } else if settings.Mode == "unlimited" { // Unlimited mode: Allow everything except explicitly blocked types From c6967d0b510a9be0cb35072720a1417f8067bae7 Mon Sep 17 00:00:00 2001 From: Maphikza Date: Fri, 11 Oct 2024 16:38:31 +0200 Subject: [PATCH 03/21] Fixing data pull bug. --- lib/stores/stats_stores/statistics_store_gorm.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/stores/stats_stores/statistics_store_gorm.go b/lib/stores/stats_stores/statistics_store_gorm.go index aa3bd8d..cc39d2c 100644 --- a/lib/stores/stats_stores/statistics_store_gorm.go +++ b/lib/stores/stats_stores/statistics_store_gorm.go @@ -479,7 +479,7 @@ func (store *GormStatisticsStore) FetchProfilesTimeSeriesData(startDate, endDate COUNT(CASE WHEN dht_key THEN 1 ELSE NULL END) as dht_key, COUNT(CASE WHEN lightning_addr AND dht_key THEN 1 ELSE NULL END) as lightning_and_dht FROM user_profiles - WHERE strftime('%Y-%m', timestamp) >= ? AND strftime('%Y-%m', timestamp) < ? + WHERE strftime('%Y-%m', timestamp) >= ? AND strftime('%Y-%m', timestamp) <= ? GROUP BY month ORDER BY month ASC; `, startDate, endDate).Scan(&data).Error From c7adc01543899d6ab23ecbd7e449efc9eac8e28b Mon Sep 17 00:00:00 2001 From: Maphikza Date: Sat, 12 Oct 2024 13:52:18 +0200 Subject: [PATCH 04/21] updating relays settings type and relay setting handler. --- lib/types.go | 31 ++++++++++++++----------------- lib/web/handler_relay_settings.go | 3 --- services/server/port/main.go | 28 ++++++++++++++-------------- 3 files changed, 28 insertions(+), 34 deletions(-) diff --git a/lib/types.go b/lib/types.go index fee1900..ee65fc3 100644 --- a/lib/types.go +++ b/lib/types.go @@ -169,23 +169,20 @@ type BitcoinRate struct { } type RelaySettings struct { - Mode string `json:"mode"` - Protocol []string `json:"protocol"` - Chunked []string `json:"chunked"` - Chunksize string `json:"chunksize"` - MaxFileSize int `json:"maxFileSize"` - MaxFileSizeUnit string `json:"maxFileSizeUnit"` - Kinds []string `json:"kinds"` - DynamicKinds []string `json:"dynamicKinds"` - Photos []string `json:"photos"` - Videos []string `json:"videos"` - GitNestr []string `json:"gitNestr"` - Audio []string `json:"audio"` - IsKindsActive bool `json:"isKindsActive"` - IsPhotosActive bool `json:"isPhotosActive"` - IsVideosActive bool `json:"isVideosActive"` - IsGitNestrActive bool `json:"isGitNestrActive"` - IsAudioActive bool `json:"isAudioActive"` + Mode string `json:"mode"` + Protocol []string `json:"protocol"` + Kinds []string `json:"kinds"` + DynamicKinds []string `json:"dynamicKinds"` + Photos []string `json:"photos"` + Videos []string `json:"videos"` + GitNestr []string `json:"gitNestr"` + Audio []string `json:"audio"` + IsKindsActive bool `json:"isKindsActive"` + IsPhotosActive bool `json:"isPhotosActive"` + IsVideosActive bool `json:"isVideosActive"` + IsGitNestrActive bool `json:"isGitNestrActive"` + IsAudioActive bool `json:"isAudioActive"` + IsFileStorageActive bool `json:"isFileStorageActive"` // New fields for the file type lists PhotoTypes []string `json:"photoTypes"` diff --git a/lib/web/handler_relay_settings.go b/lib/web/handler_relay_settings.go index 1b6d215..b1a9586 100644 --- a/lib/web/handler_relay_settings.go +++ b/lib/web/handler_relay_settings.go @@ -90,9 +90,6 @@ func getRelaySettings(c *fiber.Ctx) error { if relaySettings.Protocol == nil { relaySettings.Protocol = []string{} } - if relaySettings.Chunked == nil { - relaySettings.Chunked = []string{} - } log.Println("Fetched relay settings:", relaySettings) diff --git a/services/server/port/main.go b/services/server/port/main.go index 6f6e64a..0cf92b7 100644 --- a/services/server/port/main.go +++ b/services/server/port/main.go @@ -88,20 +88,20 @@ func init() { // Set default relay settings (including Mode) viper.SetDefault("relay_settings", map[string]interface{}{ - "Mode": "smart", // Default mode to "smart" - "IsKindsActive": false, // Default to false for activity flags - "IsPhotosActive": false, - "IsVideosActive": false, - "IsGitNestrActive": false, - "IsAudioActive": false, - "Kinds": []string{}, // Default empty arrays for list fields - "DynamicKinds": []string{}, - "Photos": []string{}, - "Videos": []string{}, - "GitNestr": []string{}, - "Audio": []string{}, - "Protocol": []string{}, // Default empty Protocol and Chunked lists - "Chunked": []string{}, + "Mode": "smart", // Default mode to "smart" + "IsKindsActive": false, // Default to false for activity flags + "IsPhotosActive": false, + "IsVideosActive": false, + "IsGitNestrActive": false, + "IsAudioActive": false, + "isFileStorageActive": false, + "Kinds": []string{}, // Default empty arrays for list fields + "DynamicKinds": []string{}, + "Photos": []string{}, + "Videos": []string{}, + "GitNestr": []string{}, + "Audio": []string{}, + "Protocol": []string{}, // Default empty Protocol and Chunked lists // New default file type lists for Photos, Videos, and Audio "PhotoFileTypes": []string{ From 9e116e6f7a4a3a28b21c473b7f41b3e331e3144a Mon Sep 17 00:00:00 2001 From: Maphikza Date: Wed, 16 Oct 2024 11:45:48 +0200 Subject: [PATCH 05/21] Changing panel settings management via viper --- lib/web/handler_relay_settings.go | 70 ++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 25 deletions(-) diff --git a/lib/web/handler_relay_settings.go b/lib/web/handler_relay_settings.go index b1a9586..187ea2d 100644 --- a/lib/web/handler_relay_settings.go +++ b/lib/web/handler_relay_settings.go @@ -39,39 +39,59 @@ func updateRelaySettings(c *fiber.Ctx) error { return c.Status(fiber.StatusInternalServerError).SendString("Internal Server Error") } - // Check boolean flags and set corresponding arrays to empty if false - if !relaySettings.IsKindsActive { - relaySettings.Kinds = []string{} - relaySettings.DynamicKinds = []string{} + // Apply logic for boolean flags + applyBooleanFlags(&relaySettings) + + // Update Viper configuration + if err := updateViperConfig(relaySettings); err != nil { + log.Printf("Error updating config: %s", err) + return c.Status(fiber.StatusInternalServerError).SendString("Failed to update settings") } - if !relaySettings.IsPhotosActive { - relaySettings.Photos = []string{} + + log.Println("Stored relay settings:", relaySettings) + + return c.SendStatus(fiber.StatusOK) +} + +func applyBooleanFlags(settings *types.RelaySettings) { + if !settings.IsKindsActive { + settings.Kinds = []string{} + settings.DynamicKinds = []string{} } - if !relaySettings.IsVideosActive { - relaySettings.Videos = []string{} + if !settings.IsPhotosActive { + settings.Photos = []string{} } - if !relaySettings.IsGitNestrActive { - relaySettings.GitNestr = []string{} + if !settings.IsVideosActive { + settings.Videos = []string{} } - if !relaySettings.IsAudioActive { - relaySettings.Audio = []string{} + if !settings.IsGitNestrActive { + settings.GitNestr = []string{} } - if relaySettings.Mode == "smart" { - relaySettings.DynamicKinds = []string{} + if !settings.IsAudioActive { + settings.Audio = []string{} } - - // Store in Viper - viper.Set("relay_settings", relaySettings) - - // Save the changes to the configuration file - if err := viper.WriteConfig(); err != nil { - log.Printf("Error writing config: %s", err) - return c.Status(fiber.StatusInternalServerError).SendString("Failed to update settings") + if settings.Mode == "smart" { + settings.DynamicKinds = []string{} } +} - log.Println("Stored relay settings:", relaySettings) - - return c.SendStatus(fiber.StatusOK) +func updateViperConfig(settings types.RelaySettings) error { + viper.Set("relay_settings.Mode", settings.Mode) + viper.Set("relay_settings.IsKindsActive", settings.IsKindsActive) + viper.Set("relay_settings.IsPhotosActive", settings.IsPhotosActive) + viper.Set("relay_settings.IsVideosActive", settings.IsVideosActive) + viper.Set("relay_settings.IsGitNestrActive", settings.IsGitNestrActive) + viper.Set("relay_settings.IsAudioActive", settings.IsAudioActive) + viper.Set("relay_settings.IsFileStorageActive", settings.IsFileStorageActive) + viper.Set("relay_settings.Kinds", settings.Kinds) + viper.Set("relay_settings.DynamicKinds", settings.DynamicKinds) + viper.Set("relay_settings.Photos", settings.Photos) + viper.Set("relay_settings.Videos", settings.Videos) + viper.Set("relay_settings.GitNestr", settings.GitNestr) + viper.Set("relay_settings.Audio", settings.Audio) + viper.Set("relay_settings.Protocol", settings.Protocol) + + return viper.WriteConfig() } func getRelaySettings(c *fiber.Ctx) error { From d00fb0f6c602d2d34a82336aad7c06cdc1e821a6 Mon Sep 17 00:00:00 2001 From: Maphikza Date: Wed, 16 Oct 2024 11:49:18 +0200 Subject: [PATCH 06/21] removing redundant debug logs. --- lib/web/handler_relay_settings.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/web/handler_relay_settings.go b/lib/web/handler_relay_settings.go index 187ea2d..cb90c6c 100644 --- a/lib/web/handler_relay_settings.go +++ b/lib/web/handler_relay_settings.go @@ -17,8 +17,6 @@ func updateRelaySettings(c *fiber.Ctx) error { return c.Status(400).SendString(err.Error()) } - log.Println("Received data:", data) - relaySettingsData, ok := data["relay_settings"] if !ok { log.Println("Relay settings data not provided") @@ -32,8 +30,6 @@ func updateRelaySettings(c *fiber.Ctx) error { return c.Status(fiber.StatusInternalServerError).SendString("Internal Server Error") } - log.Println("Received relay settings JSON:", string(relaySettingsJSON)) - if err := json.Unmarshal(relaySettingsJSON, &relaySettings); err != nil { log.Println("Error unmarshaling relay settings:", err) return c.Status(fiber.StatusInternalServerError).SendString("Internal Server Error") From 9a6b2f6a70ee593308b4a6139008f39ea91dff47 Mon Sep 17 00:00:00 2001 From: Maphikza Date: Wed, 16 Oct 2024 13:15:27 +0200 Subject: [PATCH 07/21] Updating relay settings type to include app buckets --- lib/types.go | 2 ++ lib/web/handler_relay_settings.go | 22 +++++++++++++++++++++- services/server/port/main.go | 2 ++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/types.go b/lib/types.go index ee65fc3..a7e9148 100644 --- a/lib/types.go +++ b/lib/types.go @@ -183,6 +183,8 @@ type RelaySettings struct { IsGitNestrActive bool `json:"isGitNestrActive"` IsAudioActive bool `json:"isAudioActive"` IsFileStorageActive bool `json:"isFileStorageActive"` + AppBuckets []string `json:"appBuckets"` + DynamicAppBuckets []string `json:"dynamicAppBuckets"` // New fields for the file type lists PhotoTypes []string `json:"photoTypes"` diff --git a/lib/web/handler_relay_settings.go b/lib/web/handler_relay_settings.go index cb90c6c..a6fdf60 100644 --- a/lib/web/handler_relay_settings.go +++ b/lib/web/handler_relay_settings.go @@ -66,6 +66,17 @@ func applyBooleanFlags(settings *types.RelaySettings) { if !settings.IsAudioActive { settings.Audio = []string{} } + + if settings.AppBuckets == nil { + settings.AppBuckets = []string{} + } + + log.Println("Dynamic app buckets: ", settings.DynamicAppBuckets) + + if settings.DynamicAppBuckets == nil { + settings.DynamicAppBuckets = []string{} + } + if settings.Mode == "smart" { settings.DynamicKinds = []string{} } @@ -86,6 +97,8 @@ func updateViperConfig(settings types.RelaySettings) error { viper.Set("relay_settings.GitNestr", settings.GitNestr) viper.Set("relay_settings.Audio", settings.Audio) viper.Set("relay_settings.Protocol", settings.Protocol) + viper.Set("relay_settings.AppBuckets", settings.AppBuckets) + viper.Set("relay_settings.DynamicAppBuckets", settings.DynamicAppBuckets) return viper.WriteConfig() } @@ -102,11 +115,18 @@ func getRelaySettings(c *fiber.Ctx) error { return c.Status(fiber.StatusInternalServerError).SendString("Failed to fetch settings") } - // Ensure Protocol and Chunked are arrays if relaySettings.Protocol == nil { relaySettings.Protocol = []string{} } + if relaySettings.AppBuckets == nil { + relaySettings.AppBuckets = []string{} + } + + if relaySettings.DynamicAppBuckets == nil { + relaySettings.DynamicAppBuckets = []string{} + } + log.Println("Fetched relay settings:", relaySettings) return c.JSON(fiber.Map{ diff --git a/services/server/port/main.go b/services/server/port/main.go index 0cf92b7..a046d45 100644 --- a/services/server/port/main.go +++ b/services/server/port/main.go @@ -102,6 +102,8 @@ func init() { "GitNestr": []string{}, "Audio": []string{}, "Protocol": []string{}, // Default empty Protocol and Chunked lists + "AppBuckets": []string{}, + "DynamicAppBuckets": []string{}, // New default file type lists for Photos, Videos, and Audio "PhotoFileTypes": []string{ From 280b74c3c4c953ec5ad20eeb6e2579a39fbdb235 Mon Sep 17 00:00:00 2001 From: Maphikza Date: Wed, 30 Oct 2024 21:04:08 +0200 Subject: [PATCH 08/21] adding subscription filter and changing address handling to be done with stats db as sql is easier to update and query --- lib/handlers/blossom/blossom.go | 10 ++ .../kind411/subscriptionEventsCreator.go | 3 +- lib/stores/graviton/graviton.go | 45 ++++++-- lib/stores/memory/memory.go | 31 ++++++ lib/stores/statistics_store.go | 3 + .../stats_stores/statistics_store_gorm.go | 105 ++++++++++++++++++ lib/stores/stores.go | 1 + lib/transports/websocket/auth.go | 10 +- lib/transports/websocket/server.go | 7 +- lib/types.go | 13 ++- lib/web/handler_wallet_addresses.go | 26 +++-- lib/web/server.go | 10 ++ services/server/port/main.go | 57 ++++++++++ 13 files changed, 295 insertions(+), 26 deletions(-) diff --git a/lib/handlers/blossom/blossom.go b/lib/handlers/blossom/blossom.go index 204ab00..56a055b 100644 --- a/lib/handlers/blossom/blossom.go +++ b/lib/handlers/blossom/blossom.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "encoding/hex" "fmt" + "time" "github.com/HORNET-Storage/hornet-storage/lib/stores" "github.com/gofiber/fiber/v2" @@ -35,6 +36,15 @@ func (s *Server) getBlob(c *fiber.Ctx) error { func (s *Server) uploadBlob(c *fiber.Ctx) error { pubkey := c.Query("pubkey") + subscriber, err := s.storage.GetSubscriber(pubkey) + if err != nil { + return err + } + + if time.Now().After(subscriber.EndDate) { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": "The subscription is inactive, unable to upload file."}) + } + data := c.Body() checkHash := sha256.Sum256(data) diff --git a/lib/handlers/nostr/kind411/subscriptionEventsCreator.go b/lib/handlers/nostr/kind411/subscriptionEventsCreator.go index 865fdd9..dfbacbb 100644 --- a/lib/handlers/nostr/kind411/subscriptionEventsCreator.go +++ b/lib/handlers/nostr/kind411/subscriptionEventsCreator.go @@ -211,10 +211,11 @@ func CreateNIP88Event(relayPrivKey *btcec.PrivateKey, userPubKey string, store * tags := []nostr.Tag{ {"subscription-duration", "1 month"}, - {"npub", userPubKey}, + {"p", userPubKey}, {"relay-bitcoin-address", addr.Address}, // Add Lightning invoice if applicable {"relay-dht-key", viper.GetString("RelayDHTkey")}, + {"subscription_status", "inactive"}, } for _, tier := range subscriptionTiers { diff --git a/lib/stores/graviton/graviton.go b/lib/stores/graviton/graviton.go index f6cfddc..0b8460e 100644 --- a/lib/stores/graviton/graviton.go +++ b/lib/stores/graviton/graviton.go @@ -9,6 +9,7 @@ import ( "sort" "strconv" "strings" + "sync" "time" "unicode" @@ -35,6 +36,7 @@ const ( type GravitonStore struct { Database *graviton.Store StatsDatabase stores.StatisticsStore + mu sync.Mutex } func (store *GravitonStore) InitStore(basepath string, args ...interface{}) error { @@ -89,6 +91,7 @@ func (store *GravitonStore) QueryDag(filter map[string]string) ([]string, error) for bucket, key := range filter { cacheBucket := fmt.Sprintf("cache:%s", bucket) + log.Printf("Processing buckect: %s with key %s", cacheBucket, key) cacheTree, err := snapshot.GetTree(cacheBucket) if err == nil { value, err := cacheTree.Get([]byte(key)) @@ -106,36 +109,52 @@ func (store *GravitonStore) QueryDag(filter map[string]string) ([]string, error) } func (store *GravitonStore) SaveAddress(addr *types.Address) error { - // Load the snapshot and get the "relay_addresses" tree + // Synchronize to prevent concurrent writes + store.mu.Lock() + defer store.mu.Unlock() + + // Load the latest snapshot snapshot, err := store.Database.LoadSnapshot(0) if err != nil { + log.Printf("Error loading snapshot: %v", err) return fmt.Errorf("failed to load snapshot: %v", err) } + log.Println("Loaded latest snapshot") + // Use or create the 'relay_addresses' tree as needed addressTree, err := snapshot.GetTree("relay_addresses") if err != nil { + log.Printf("Error getting address tree: %v", err) return fmt.Errorf("failed to get address tree: %v", err) } - // Marshal the address into JSON + key := addr.Address + log.Printf("Attempting to save address: %s", addr.Address) + + existingData, _ := addressTree.Get([]byte(key)) + if len(existingData) > 0 { + log.Printf("Address %s already exists; skipping save", key) + return nil + } + addressData, err := json.Marshal(addr) if err != nil { + log.Printf("Error marshaling address: %v", err) return fmt.Errorf("failed to marshal address: %v", err) } - // Use the index as the key for storing the address - key := addr.Index - - // Store the address data in the tree if err := addressTree.Put([]byte(key), addressData); err != nil { + log.Printf("Error putting address in Graviton store: %v", err) return fmt.Errorf("failed to put address in Graviton store: %v", err) } - // Commit the tree to persist the changes - if _, err := graviton.Commit(addressTree); err != nil { + _, err = graviton.Commit(addressTree) + if err != nil { + log.Printf("Error committing address tree: %v", err) return fmt.Errorf("failed to commit address tree: %v", err) } + log.Println("Address saved successfully") return nil } @@ -993,6 +1012,8 @@ func ContainsAny(tags nostr.Tags, tagName string, values []string) bool { } func (store *GravitonStore) SaveSubscriber(subscriber *types.Subscriber) error { + store.mu.Lock() + defer store.mu.Unlock() // Load the snapshot and get the "subscribers" tree snapshot, err := store.Database.LoadSnapshot(0) if err != nil { @@ -1027,6 +1048,9 @@ func (store *GravitonStore) SaveSubscriber(subscriber *types.Subscriber) error { } func (store *GravitonStore) GetSubscriberByAddress(address string) (*types.Subscriber, error) { + store.mu.Lock() + defer store.mu.Unlock() + snapshot, err := store.Database.LoadSnapshot(0) if err != nil { return nil, fmt.Errorf("failed to load snapshot: %v", err) @@ -1061,6 +1085,9 @@ func (store *GravitonStore) GetSubscriberByAddress(address string) (*types.Subsc } func (store *GravitonStore) GetSubscriber(npub string) (*types.Subscriber, error) { + store.mu.Lock() + defer store.mu.Unlock() + snapshot, err := store.Database.LoadSnapshot(0) if err != nil { return nil, fmt.Errorf("failed to load snapshot: %v", err) @@ -1111,6 +1138,8 @@ func (store *GravitonStore) AllocateBitcoinAddress(npub string) (*types.Address, log.Printf("Error unmarshaling address: %v. Skipping this address.", err) continue } + log.Println("Address Index: ", addr.Index) + log.Println("Address: ", addr.Address) if addr.Status == AddressStatusAvailable { // Allocate the address to the subscriber now := time.Now() diff --git a/lib/stores/memory/memory.go b/lib/stores/memory/memory.go index 0cb4645..27f336c 100644 --- a/lib/stores/memory/memory.go +++ b/lib/stores/memory/memory.go @@ -345,6 +345,37 @@ func (store *GravitonMemoryStore) StoreDag(dag *types.DagData) error { return stores.StoreDag(store, dag) } +func (store *GravitonMemoryStore) GetMasterBucketList(key string) ([]string, error) { + // Load a snapshot from the in-memory database + snapshot, err := store.Database.LoadSnapshot(0) + if err != nil { + return nil, err + } + + // Access the "mbl" tree within the snapshot to retrieve the master bucket list + tree, err := snapshot.GetTree("mbl") + if err != nil { + return nil, err + } + + var masterBucketList []string + + // Retrieve the list of buckets corresponding to the given key + bytes, err := tree.Get([]byte(fmt.Sprintf("mbl_%s", key))) + if bytes == nil || err != nil { + // If the key doesn't exist or an error occurred, initialize an empty list + masterBucketList = []string{} + } else { + // Unmarshal the list of buckets from the retrieved bytes + err = cbor.Unmarshal(bytes, &masterBucketList) + if err != nil { + return nil, err + } + } + + return masterBucketList, nil +} + func (store *GravitonMemoryStore) QueryEvents(filter nostr.Filter) ([]*nostr.Event, error) { log.Println("Processing filter:", filter) diff --git a/lib/stores/statistics_store.go b/lib/stores/statistics_store.go index 9cdd615..2f549f1 100644 --- a/lib/stores/statistics_store.go +++ b/lib/stores/statistics_store.go @@ -73,4 +73,7 @@ type StatisticsStore interface { FetchGitNestrCount(gitNestr []string) (int, error) FetchAudioCount() (int, error) FetchMiscCount() (int, error) + + SaveSubcriberAddress(address *types.SubscriberAddress) error + AllocateBitcoinAddress(npub string) (*types.Address, error) } diff --git a/lib/stores/stats_stores/statistics_store_gorm.go b/lib/stores/stats_stores/statistics_store_gorm.go index cc39d2c..cbc1476 100644 --- a/lib/stores/stats_stores/statistics_store_gorm.go +++ b/lib/stores/stats_stores/statistics_store_gorm.go @@ -21,6 +21,12 @@ type GormStatisticsStore struct { DB *gorm.DB } +const ( + AddressStatusAvailable = "available" + AddressStatusAllocated = "allocated" + AddressStatusUsed = "used" +) + // InitStore initializes the GORM DB (can be swapped for another DB). func (store *GormStatisticsStore) InitStore(basepath string, args ...interface{}) error { var err error @@ -46,6 +52,7 @@ func (store *GormStatisticsStore) InitStore(basepath string, args ...interface{} &types.Audio{}, &types.PendingTransaction{}, &types.ActiveToken{}, + &types.SubscriberAddress{}, ) if err != nil { return fmt.Errorf("failed to migrate database schema: %v", err) @@ -764,6 +771,104 @@ func (store *GormStatisticsStore) SaveAddress(address *types.WalletAddress) erro return store.DB.Create(address).Error } +func (store *GormStatisticsStore) SaveSubcriberAddress(address *types.SubscriberAddress) error { + // Convert types.Address to subscriptionAddress + subscriptionAddress := types.SubscriberAddress{ + Index: address.Index, + Address: address.Address, + WalletName: address.WalletName, + Status: address.Status, + AllocatedAt: address.AllocatedAt, + Npub: address.Npub, + } + + // Check if the address already exists + var existingAddress types.SubscriberAddress + result := store.DB.Where("address = ?", subscriptionAddress.Address).First(&existingAddress) + + // Handle potential errors from the query + if result.Error != nil && result.Error != gorm.ErrRecordNotFound { + log.Printf("Error querying existing address: %v", result.Error) + return result.Error + } + + // If the address already exists, log and skip the insert + if result.RowsAffected > 0 { + log.Printf("Address %s already exists, skipping save.", subscriptionAddress.Address) + return nil + } + + // Set defaults if needed + if subscriptionAddress.Status == "" { + subscriptionAddress.Status = "available" + } + if subscriptionAddress.AllocatedAt == nil { + now := time.Now() + subscriptionAddress.AllocatedAt = &now + } + + // Attempt to create the new address in the database + if err := store.DB.Create(&subscriptionAddress).Error; err != nil { + log.Printf("Error saving new address: %v", err) + return err + } + + log.Printf("Address %s saved successfully.", subscriptionAddress.Address) + return nil +} + +func (store *GormStatisticsStore) AllocateBitcoinAddress(npub string) (*types.Address, error) { + // Begin a new transaction to handle concurrency safely + tx := store.DB.Begin() + + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + // Find the first available address + var subscriptionAddress types.SubscriberAddress + err := tx.Where("status = ?", AddressStatusAvailable). + Order("id"). + First(&subscriptionAddress).Error + + if err != nil { + tx.Rollback() + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("no available addresses") + } + return nil, fmt.Errorf("failed to query available addresses: %v", err) + } + + // Update the address fields to allocate it to the user + now := time.Now() + subscriptionAddress.Status = AddressStatusAllocated + subscriptionAddress.AllocatedAt = &now + subscriptionAddress.Npub = npub + + // Save the updated address + if err := tx.Save(&subscriptionAddress).Error; err != nil { + tx.Rollback() + return nil, fmt.Errorf("failed to save allocated address: %v", err) + } + + // Commit the transaction + if err := tx.Commit().Error; err != nil { + return nil, fmt.Errorf("failed to commit transaction: %v", err) + } + + // Convert subscriptionAddress back to types.Address for returning + return &types.Address{ + Index: subscriptionAddress.Index, + Address: subscriptionAddress.Address, + WalletName: subscriptionAddress.WalletName, + Status: subscriptionAddress.Status, + AllocatedAt: subscriptionAddress.AllocatedAt, + Npub: subscriptionAddress.Npub, + }, nil +} + // GetLatestWalletBalance retrieves the latest wallet balance from the database func (store *GormStatisticsStore) GetLatestWalletBalance() (types.WalletBalance, error) { var latestBalance types.WalletBalance diff --git a/lib/stores/stores.go b/lib/stores/stores.go index 3c26f83..751111c 100644 --- a/lib/stores/stores.go +++ b/lib/stores/stores.go @@ -31,6 +31,7 @@ type Store interface { StoreBlob(data []byte, hash []byte, publicKey string) error GetBlob(hash string) ([]byte, error) DeleteBlob(hash string) error + GetMasterBucketList(key string) ([]string, error) // Panel GetSubscriber(npub string) (*types.Subscriber, error) diff --git a/lib/transports/websocket/auth.go b/lib/transports/websocket/auth.go index c02dc8a..1098ee2 100644 --- a/lib/transports/websocket/auth.go +++ b/lib/transports/websocket/auth.go @@ -153,7 +153,7 @@ func handleAuthMessage(c *websocket.Conn, env *nostr.AuthEnvelope, challenge str // Allocate the address to a specific npub (subscriber) func generateUniqueBitcoinAddress(store stores.Store, npub string) (*types.Address, error) { // Use the store method to allocate the address - address, err := store.AllocateBitcoinAddress(npub) + address, err := store.GetStatsStore().AllocateBitcoinAddress(npub) if err != nil { return nil, fmt.Errorf("failed to allocate Bitcoin address: %v", err) @@ -172,9 +172,9 @@ func CreateNIP88Event(relayPrivKey *btcec.PrivateKey, userPubKey string, store s } subscriptionTiers := []types.SubscriptionTier{ - {DataLimit: "1 GB per month", Price: "10000"}, - {DataLimit: "5 GB per month", Price: "40000"}, - {DataLimit: "10 GB per month", Price: "70000"}, + {DataLimit: "1 GB per month", Price: "8000"}, + {DataLimit: "5 GB per month", Price: "10000"}, + {DataLimit: "10 GB per month", Price: "15000"}, } uniqueAddress, err := generateUniqueBitcoinAddress(store, userPubKey) @@ -230,7 +230,7 @@ func CreateNIP88Event(relayPrivKey *btcec.PrivateKey, userPubKey string, store s func getExistingNIP88Event(store stores.Store, userPubKey string) (*nostr.Event, error) { filter := nostr.Filter{ - Kinds: []int{88}, + Kinds: []int{764}, Tags: nostr.TagMap{ "p": []string{userPubKey}, }, diff --git a/lib/transports/websocket/server.go b/lib/transports/websocket/server.go index 3b1103f..f6f6d7d 100644 --- a/lib/transports/websocket/server.go +++ b/lib/transports/websocket/server.go @@ -13,6 +13,7 @@ import ( "strings" "github.com/HORNET-Storage/hornet-storage/lib/signing" + "github.com/HORNET-Storage/hornet-storage/lib/stores/graviton" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" @@ -22,14 +23,14 @@ import ( "github.com/spf13/viper" "github.com/HORNET-Storage/hornet-storage/lib/handlers/blossom" - "github.com/HORNET-Storage/hornet-storage/lib/stores" + // "github.com/HORNET-Storage/hornet-storage/lib/stores" ) type connectionState struct { authenticated bool } -func BuildServer(store stores.Store) *fiber.App { +func BuildServer(store *graviton.GravitonStore) *fiber.App { app := fiber.New() // Middleware for handling relay information requests @@ -214,7 +215,7 @@ func PackRelayForSig(nr *NIP11RelayInfo) []byte { return packed } -func processWebSocketMessage(c *websocket.Conn, challenge string, state *connectionState, store stores.Store) error { +func processWebSocketMessage(c *websocket.Conn, challenge string, state *connectionState, store *graviton.GravitonStore) error { _, message, err := c.ReadMessage() if err != nil { return fmt.Errorf("read error: %w", err) diff --git a/lib/types.go b/lib/types.go index a7e9148..27912ca 100644 --- a/lib/types.go +++ b/lib/types.go @@ -253,7 +253,7 @@ type ReplaceTransactionRequest struct { // Address structure to be stored in Graviton type Address struct { - Index string `json:"index,string"` // Use string tag to handle string-encoded integers + Index string `json:"index"` // Use string tag to handle string-encoded integers Address string `json:"address"` WalletName string `json:"wallet_name"` Status string `json:"status"` @@ -261,6 +261,17 @@ type Address struct { Npub string `json:"npub,omitempty"` } +// subscriptionAddress represents the GORM-compatible model for storing addresses +type SubscriberAddress struct { + ID uint `gorm:"primaryKey"` + Index string `gorm:"not null"` + Address string `gorm:"not null;unique"` + WalletName string `gorm:"not null"` + Status string `gorm:"default:'available'"` + AllocatedAt *time.Time `gorm:"default:null"` + Npub string `gorm:"default:null"` +} + // type User struct { // ID uint `gorm:"primaryKey"` // FirstName string diff --git a/lib/web/handler_wallet_addresses.go b/lib/web/handler_wallet_addresses.go index fa270fe..ade74cd 100644 --- a/lib/web/handler_wallet_addresses.go +++ b/lib/web/handler_wallet_addresses.go @@ -1,7 +1,10 @@ package web import ( + "encoding/json" + "fmt" "log" + "time" types "github.com/HORNET-Storage/hornet-storage/lib" "github.com/HORNET-Storage/hornet-storage/lib/stores" @@ -18,15 +21,20 @@ const ( func saveWalletAddresses(c *fiber.Ctx, store stores.Store) error { log.Println("Addresses request received") - var addresses []types.Address - // Parse the JSON request body - if err := c.BodyParser(&addresses); err != nil { + body := c.Body() + log.Println("Raw JSON Body:", string(body)) + + var addresses []types.Address + if err := json.Unmarshal(body, &addresses); err != nil { + log.Printf("Error unmarshaling JSON directly: %v", err) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Cannot parse JSON", }) } + log.Println("Addresses: ", addresses) + // Get the expected wallet name from the configuration expectedWalletName := viper.GetString("wallet_name") if expectedWalletName == "" { @@ -66,15 +74,17 @@ func saveWalletAddresses(c *fiber.Ctx, store stores.Store) error { } // Add the address to the Graviton store - gravitonAddress := &types.Address{ - Index: addr.Index, + // Add the address to the Graviton store with default values + subscriptionAddress := &types.SubscriberAddress{ + Index: fmt.Sprint(addr.Index), Address: addr.Address, WalletName: addr.WalletName, - Status: AddressStatusAvailable, - AllocatedAt: nil, + Status: AddressStatusAvailable, // Default status + AllocatedAt: &time.Time{}, // Use zero time if not allocated + Npub: "", // Default to empty string } - if err := store.SaveAddress(gravitonAddress); err != nil { + if err := store.GetStatsStore().SaveSubcriberAddress(subscriptionAddress); err != nil { log.Printf("Error saving address to Graviton store: %v", err) } } diff --git a/lib/web/server.go b/lib/web/server.go index 6feec8d..1fdf7fe 100644 --- a/lib/web/server.go +++ b/lib/web/server.go @@ -55,6 +55,7 @@ func StartServer(store stores.Store) error { return updateWalletTransactions(c, store) }) walletRoutes.Post("/addresses", func(c *fiber.Ctx) error { + log.Println("Received addresses.") return saveWalletAddresses(c, store) // Pass the store instance }) @@ -111,6 +112,15 @@ func StartServer(store stores.Store) error { }) secured.Post("/refresh-token", refreshToken) + // In your StartServer function + secured.Get("/media", func(c *fiber.Ctx) error { + return GetMedia(c, store) + }) + + secured.Get("/media/content/:hash", func(c *fiber.Ctx) error { + return GetMediaContent(c, store) + }) + port := viper.GetString("port") p, err := strconv.Atoi(port) if err != nil { diff --git a/services/server/port/main.go b/services/server/port/main.go index a046d45..0478984 100644 --- a/services/server/port/main.go +++ b/services/server/port/main.go @@ -3,8 +3,10 @@ package main import ( "context" "crypto/rand" + "crypto/sha512" "encoding/hex" "encoding/json" + "fmt" "log" "os" "os/signal" @@ -167,12 +169,67 @@ func generateRandomAPIKey() (string, error) { return hex.EncodeToString(bytes), nil } +func generateDHTKey(privateKeyHex string) (string, error) { + // Convert hex string to bytes + privateKeyBytes, err := hex.DecodeString(privateKeyHex) + if err != nil { + return "", fmt.Errorf("failed to decode private key hex: %v", err) + } + + // Ensure we have the correct length + if len(privateKeyBytes) != 32 { + return "", fmt.Errorf("invalid private key length: expected 32 bytes, got %d", len(privateKeyBytes)) + } + + // Create a copy for clamping + clampedPrivateKey := make([]byte, len(privateKeyBytes)) + copy(clampedPrivateKey, privateKeyBytes) + + // Apply clamping as per Ed25519 specification + clampedPrivateKey[0] &= 248 // Clear the lowest 3 bits + clampedPrivateKey[31] &= 127 // Clear the highest bit + clampedPrivateKey[31] |= 64 // Set the second highest bit + + // Calculate hash using SHA-512 + hash := sha512.Sum512(clampedPrivateKey[:32]) + + // In Ed25519, the first 32 bytes of the hash are used as the scalar + // and the public key is derived using this scalar + scalar := hash[:32] + + // For DHT key, we'll use the hex encoding of the scalar + // This matches the behavior of the TypeScript implementation + dhtKey := hex.EncodeToString(scalar) + + return dhtKey, nil +} + func main() { ctx := context.Background() wg := new(sync.WaitGroup) serializedPrivateKey := viper.GetString("private_key") + if serializedPrivateKey != "" { + // Generate DHT key from private key + dhtKey, err := generateDHTKey(serializedPrivateKey) + if err != nil { + log.Printf("Failed to generate DHT key: %v", err) + } else { + err = viper.ReadInConfig() + if err != nil { + log.Println("Error reading viper config: ", err) + } + viper.Set("RelayDHTkey", dhtKey) + err = viper.WriteConfig() + if err != nil { + log.Println("Error reading viper config: ", err) + } + log.Println("DHT key: ", dhtKey) + + } + } + // Generate a new private key and save it to viper config if one doesn't exist if serializedPrivateKey == "" { newKey, err := signing.GeneratePrivateKey() From 70304d055f5487d2e166fe7bf33ebb3fdf98c230 Mon Sep 17 00:00:00 2001 From: Maphikza Date: Wed, 30 Oct 2024 21:04:55 +0200 Subject: [PATCH 09/21] Adding media handler --- lib/web/handler_get_media.go | 317 +++++++++++++++++++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 lib/web/handler_get_media.go diff --git a/lib/web/handler_get_media.go b/lib/web/handler_get_media.go new file mode 100644 index 0000000..c1f44a8 --- /dev/null +++ b/lib/web/handler_get_media.go @@ -0,0 +1,317 @@ +package web + +import ( + "encoding/hex" + "fmt" + "log" + "strconv" + "strings" + + "github.com/HORNET-Storage/hornet-storage/lib/stores" + "github.com/gofiber/fiber/v2" +) + +// Define the supported media file types +var audioFileTypes = []string{"mp3", "wav", "ogg", "flac", "aac", "wma", "m4a", "opus", "m4b", "midi", "mp4", "webm", "3gp"} +var photoFileTypes = []string{"jpeg", "jpg", "png", "gif", "bmp", "tiff", "raw", "svg", "eps", "psd", "ai", "pdf", "webp"} +var videoFileTypes = []string{"avi", "mp4", "mov", "wmv", "mkv", "flv", "mpeg", "3gp", "webm"} + +type MediaResponse struct { + Items []MediaItem `json:"items"` + TotalCount int `json:"totalCount"` + NextCursor string `json:"nextCursor,omitempty"` +} + +type MediaItem struct { + Hash string `json:"hash"` + Name string `json:"name"` + Type string `json:"type"` + ContentHash []byte `json:"contentHash,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +func GetMedia(c *fiber.Ctx, store stores.Store) error { + log.Println("Getting Media.") + + // Parse query parameters + mediaType := c.Query("type", "all") // Default to all types + pageSize, _ := strconv.Atoi(c.Query("pageSize", "20")) // Limit the page size + cursor := c.Query("cursor", "") // For pagination + + // Validate pageSize + if pageSize <= 0 { + pageSize = 20 + } + if pageSize > 100 { + pageSize = 100 + } + + // Retrieve the list of cache buckets to use for querying the DAG + cacheBuckets, err := store.GetMasterBucketList("cache") + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": fmt.Sprintf("Failed to retrieve cache bucket list: %v", err), + }) + } + log.Printf("Cache buckets: %v", cacheBuckets) + + // Combine all media types when "all" is selected + var mediaFileTypes []string + if mediaType == "all" { + // If media type is "all", we combine all file types into one list + mediaFileTypes = append(mediaFileTypes, audioFileTypes...) + mediaFileTypes = append(mediaFileTypes, photoFileTypes...) + mediaFileTypes = append(mediaFileTypes, videoFileTypes...) + } else { + // Otherwise, filter by specific media type + switch mediaType { + case "audio": + mediaFileTypes = audioFileTypes + case "image", "photo": + mediaFileTypes = photoFileTypes + case "video": + mediaFileTypes = videoFileTypes + default: + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Unsupported media type", + }) + } + } + + // Initialize to collect all the keys found + var allKeys []string + + // Query the DAG for each cache bucket and for each media file type + for _, bucket := range cacheBuckets { + // Extract the user/app key from the cache bucket (remove the "cache:" prefix) + bucketKey := strings.TrimPrefix(bucket, "cache:") + + // Query for each media file type + for _, fileType := range mediaFileTypes { + log.Printf("Processing bucket: %s with key %s", bucketKey, fileType) + bucketFilter := map[string]string{bucketKey: fileType} + keys, err := store.QueryDag(bucketFilter) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": fmt.Sprintf("Failed to query media: %v", err), + }) + } + + // Accumulate keys across all queries + allKeys = append(allKeys, keys...) + } + } + log.Printf("Keys found: %v", allKeys) + + // Initialize the response + response := MediaResponse{ + Items: make([]MediaItem, 0), + } + + // Handle pagination using the cursor + startIndex := 0 + if cursor != "" { + for i, key := range allKeys { + if key == cursor { + startIndex = i + 1 + break + } + } + } + + // Get paginated results + endIndex := startIndex + pageSize + if endIndex > len(allKeys) { + endIndex = len(allKeys) + } + + // Set next cursor if there are more items + if endIndex < len(allKeys) { + response.NextCursor = allKeys[endIndex-1] + } + + // Retrieve each media item from the DAG + for _, key := range allKeys[startIndex:endIndex] { + leafData, err := store.RetrieveLeaf(key, key, false) // Do not include content initially + if err != nil { + continue // Skip any items that can't be retrieved + } + + // Filter by file extension + fileExtension := strings.ToLower(getFileExtension(leafData.Leaf.ItemName)) + if !isSupportedFileType(fileExtension, mediaFileTypes) { + continue // Skip unsupported file types + } + + // Create a media item from the leaf data + item := MediaItem{ + Hash: leafData.Leaf.Hash, + Name: leafData.Leaf.ItemName, + Type: fileExtension, + ContentHash: leafData.Leaf.ContentHash, + Metadata: leafData.Leaf.AdditionalData, + } + + response.Items = append(response.Items, item) + } + + response.TotalCount = len(allKeys) + return c.JSON(response) +} + +func GetMediaContent(c *fiber.Ctx, store stores.Store) error { + hash := c.Params("hash") + if hash == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Hash parameter is required", + }) + } + + // Retrieve the leaf data with the content + leafData, err := store.RetrieveLeaf(hash, hash, true) // Include content in the response + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ + "error": fmt.Sprintf("Content not found: %v", err), + }) + } + + // Set the content type based on the file extension + contentType := getContentType(leafData.Leaf.ItemName) + c.Set("Content-Type", contentType) + + log.Println("Leaf Data Content: ", hex.EncodeToString(leafData.Leaf.Content)) + + // Send the content as the response + return c.Send(leafData.Leaf.Content) +} + +// Helper function to extract file extension +func getFileExtension(filename string) string { + parts := strings.Split(filename, ".") + if len(parts) > 1 { + return parts[len(parts)-1] + } + return "" +} + +// Check if the file type is supported +func isSupportedFileType(fileExtension string, supportedFileTypes []string) bool { + for _, fileType := range supportedFileTypes { + if fileType == fileExtension { + return true + } + } + return false +} + +// Updated getContentType function +func getContentType(filename string) string { + // Extract the file extension (assuming extensions are case-insensitive) + extension := strings.ToLower(strings.TrimPrefix(strings.ToLower(filename[strings.LastIndex(filename, ".")+1:]), ".")) + + // Check if the extension belongs to an image, video, or audio + if isInList(extension, photoFileTypes) { + return getImageContentType(extension) + } else if isInList(extension, videoFileTypes) { + return getVideoContentType(extension) + } else if isInList(extension, audioFileTypes) { + return getAudioContentType(extension) + } + + // Default content type for unsupported file types + return "application/octet-stream" +} + +// Helper function to check if a file extension is in a given list +func isInList(extension string, list []string) bool { + for _, item := range list { + if item == extension { + return true + } + } + return false +} + +// Helper function to return the correct image content type +func getImageContentType(extension string) string { + switch extension { + case "jpeg", "jpg": + return "image/jpeg" + case "png": + return "image/png" + case "gif": + return "image/gif" + case "bmp": + return "image/bmp" + case "tiff": + return "image/tiff" + case "webp": + return "image/webp" + case "svg": + return "image/svg+xml" + case "pdf": + return "application/pdf" + case "eps": + return "application/postscript" + default: + return "application/octet-stream" // For unsupported image formats + } +} + +// Helper function to return the correct video content type +func getVideoContentType(extension string) string { + switch extension { + case "mp4": + return "video/mp4" + case "mov": + return "video/quicktime" + case "wmv": + return "video/x-ms-wmv" + case "mkv": + return "video/x-matroska" + case "flv": + return "video/x-flv" + case "avi": + return "video/x-msvideo" + case "mpeg": + return "video/mpeg" + case "3gp": + return "video/3gpp" + case "webm": + return "video/webm" + default: + return "application/octet-stream" // For unsupported video formats + } +} + +// Helper function to return the correct audio content type +func getAudioContentType(extension string) string { + switch extension { + case "mp3": + return "audio/mpeg" + case "wav": + return "audio/wav" + case "ogg": + return "audio/ogg" + case "flac": + return "audio/flac" + case "aac": + return "audio/aac" + case "wma": + return "audio/x-ms-wma" + case "m4a": + return "audio/mp4" + case "opus": + return "audio/opus" + case "m4b": + return "audio/x-m4b" + case "midi": + return "audio/midi" + case "webm": + return "audio/webm" + case "3gp": + return "audio/3gpp" + default: + return "application/octet-stream" // For unsupported audio formats + } +} From 3842096f66d0d5a5d5318b85348e163546000f52 Mon Sep 17 00:00:00 2001 From: Maphikza Date: Wed, 6 Nov 2024 12:36:47 +0200 Subject: [PATCH 10/21] Working to get subscription process simplified. --- lib/handlers/blossom/blossom.go | 43 +- .../kind411/subscriptionEventsCreator.go | 88 -- lib/handlers/scionic/upload/upload.go | 50 ++ lib/stores/graviton/graviton.go | 159 +++- lib/stores/memory/memory.go | 29 + .../stats_stores/statistics_store_gorm.go | 3 +- lib/stores/stores.go | 4 + lib/stores/subscriber_store.go | 60 ++ .../subscription_store_gorm.go | 646 ++++++++++++++ lib/subscription/subscription.go | 837 ++++++++++++++++++ lib/transports/websocket/auth.go | 215 ++--- lib/types.go | 83 +- lib/web/handler_update_wallet_transactions.go | 759 +++++++++++----- lib/web/handler_wallet_addresses.go | 110 ++- services/server/port/main.go | 1 + 15 files changed, 2585 insertions(+), 502 deletions(-) create mode 100644 lib/stores/subscriber_store.go create mode 100644 lib/stores/subscription_store/subscription_store_gorm.go create mode 100644 lib/subscription/subscription.go diff --git a/lib/handlers/blossom/blossom.go b/lib/handlers/blossom/blossom.go index 56a055b..457b64f 100644 --- a/lib/handlers/blossom/blossom.go +++ b/lib/handlers/blossom/blossom.go @@ -4,8 +4,10 @@ import ( "crypto/sha256" "encoding/hex" "fmt" + "log" "time" + types "github.com/HORNET-Storage/hornet-storage/lib" "github.com/HORNET-Storage/hornet-storage/lib/stores" "github.com/gofiber/fiber/v2" "github.com/nbd-wtf/go-nostr" @@ -41,8 +43,12 @@ func (s *Server) uploadBlob(c *fiber.Ctx) error { return err } - if time.Now().After(subscriber.EndDate) { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": "The subscription is inactive, unable to upload file."}) + // Validate subscription status and storage quota + if err := validateUploadEligibility(s.storage, subscriber, c.Body()); err != nil { + log.Printf("Upload validation failed for subscriber %s: %v", pubkey, err) + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ + "message": err.Error(), + }) } data := c.Body() @@ -87,3 +93,36 @@ func (s *Server) uploadBlob(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) } + +// validateUploadEligibility checks if the subscriber can upload the file +func validateUploadEligibility(store stores.Store, subscriber *types.Subscriber, data []byte) error { + // Check subscription expiration + if time.Now().After(subscriber.EndDate) { + return fmt.Errorf("subscription expired on %s", subscriber.EndDate.Format(time.RFC3339)) + } + + // Try to use subscriber store features if available + subscriberStore, ok := store.(stores.SubscriberStore) + if !ok { + // Fallback to basic validation if subscriber store is not available + return nil + } + + // Check storage quota + fileSize := int64(len(data)) + if err := subscriberStore.CheckStorageAvailability(subscriber.Npub, fileSize); err != nil { + // Get current usage for detailed error message + stats, statsErr := subscriberStore.GetSubscriberStorageStats(subscriber.Npub) + if statsErr != nil { + return fmt.Errorf("storage quota exceeded") + } + + return fmt.Errorf("storage quota exceeded: used %d of %d bytes (%.2f%%), attempting to upload %d bytes", + stats.CurrentUsageBytes, + stats.StorageLimitBytes, + stats.UsagePercentage, + fileSize) + } + + return nil +} diff --git a/lib/handlers/nostr/kind411/subscriptionEventsCreator.go b/lib/handlers/nostr/kind411/subscriptionEventsCreator.go index dfbacbb..5cbf2ff 100644 --- a/lib/handlers/nostr/kind411/subscriptionEventsCreator.go +++ b/lib/handlers/nostr/kind411/subscriptionEventsCreator.go @@ -8,14 +8,10 @@ import ( "log" "time" - types "github.com/HORNET-Storage/hornet-storage/lib" "github.com/HORNET-Storage/hornet-storage/lib/signing" "github.com/HORNET-Storage/hornet-storage/lib/stores" - stores_graviton "github.com/HORNET-Storage/hornet-storage/lib/stores/graviton" - "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/decred/dcrd/dcrec/secp256k1/v4" - "github.com/deroproject/graviton" "github.com/nbd-wtf/go-nostr" "github.com/spf13/viper" ) @@ -156,87 +152,3 @@ func serializeEventForID(event *nostr.Event) string { return compactSerialized } - -func allocateAddress(store *stores_graviton.GravitonStore) (*types.Address, error) { - ss, err := store.Database.LoadSnapshot(0) - if err != nil { - return nil, fmt.Errorf("failed to load snapshot: %v", err) - } - - addressTree, err := ss.GetTree("relay_addresses") - if err != nil { - return nil, fmt.Errorf("failed to get address tree: %v", err) - } - - cursor := addressTree.Cursor() - for _, v, err := cursor.First(); err == nil; _, v, err = cursor.Next() { - var addr types.Address - if err := json.Unmarshal(v, &addr); err != nil { - return nil, err - } - if addr.Status == AddressStatusAvailable { - now := time.Now() - addr.Status = AddressStatusAllocated - addr.AllocatedAt = &now - - value, err := json.Marshal(addr) - if err != nil { - return nil, err - } - if err := addressTree.Put([]byte(addr.Index), value); err != nil { - return nil, err - } - if _, err := graviton.Commit(addressTree); err != nil { - return nil, err - } - return &addr, nil - } - } - - return nil, fmt.Errorf("no available addresses") -} - -func CreateNIP88Event(relayPrivKey *btcec.PrivateKey, userPubKey string, store *stores_graviton.GravitonStore) (*nostr.Event, error) { - subscriptionTiers := []types.SubscriptionTier{ - {DataLimit: "1 GB per month", Price: "10,000 sats"}, - {DataLimit: "5 GB per month", Price: "40,000 sats"}, - {DataLimit: "10 GB per month", Price: "70,000 sats"}, - } - - // Allocate a new address for this subscription - addr, err := allocateAddress(store) - if err != nil { - return nil, fmt.Errorf("failed to allocate address: %v", err) - } - - tags := []nostr.Tag{ - {"subscription-duration", "1 month"}, - {"p", userPubKey}, - {"relay-bitcoin-address", addr.Address}, - // Add Lightning invoice if applicable - {"relay-dht-key", viper.GetString("RelayDHTkey")}, - {"subscription_status", "inactive"}, - } - - for _, tier := range subscriptionTiers { - tags = append(tags, nostr.Tag{"subscription-tier", tier.DataLimit, tier.Price}) - } - - event := &nostr.Event{ - PubKey: hex.EncodeToString(relayPrivKey.PubKey().SerializeCompressed()), - CreatedAt: nostr.Timestamp(time.Now().Unix()), - Kind: 764, - Tags: tags, - Content: "", - } - - hash := sha256.Sum256(event.Serialize()) - sig, err := schnorr.Sign(relayPrivKey, hash[:]) - if err != nil { - return nil, fmt.Errorf("error signing event: %v", err) - } - event.ID = hex.EncodeToString(hash[:]) - event.Sig = hex.EncodeToString(sig.Serialize()) - - return event, nil -} diff --git a/lib/handlers/scionic/upload/upload.go b/lib/handlers/scionic/upload/upload.go index 496260b..0e5ba17 100644 --- a/lib/handlers/scionic/upload/upload.go +++ b/lib/handlers/scionic/upload/upload.go @@ -2,6 +2,7 @@ package upload import ( "context" + "fmt" "log" "github.com/gofiber/contrib/websocket" @@ -82,6 +83,32 @@ func BuildUploadStreamHandler(store stores.Store, canUploadDag func(rootLeaf *me return } + // Add subscription and storage validation here + if subscriberStore, ok := store.(stores.SubscriberStore); ok { + // Get subscriber info + _, err := subscriberStore.GetSubscriber(message.PublicKey) + if err != nil { + write(utils.BuildErrorMessage("Invalid or inactive subscription", err)) + return + } + + // Pre-validate storage quota + // Note: We estimate total size based on the leaf count and average leaf size + estimatedSize := int64(len(message.Leaf.Content)) + if err := subscriberStore.CheckStorageAvailability(message.PublicKey, estimatedSize); err != nil { + stats, _ := subscriberStore.GetSubscriberStorageStats(message.PublicKey) + if stats != nil { + write(utils.BuildErrorMessage(fmt.Sprintf("Storage quota exceeded: used %d of %d bytes (%.2f%%)", + stats.CurrentUsageBytes, + stats.StorageLimitBytes, + stats.UsagePercentage), nil)) + } else { + write(utils.BuildErrorMessage("Storage quota exceeded", nil)) + } + return + } + } + err = message.Leaf.VerifyRootLeaf() if err != nil { write(utils.BuildErrorMessage("Failed to verify root leaf", err)) @@ -173,6 +200,29 @@ func BuildUploadStreamHandler(store stores.Store, canUploadDag func(rootLeaf *me return } + // Update storage usage after successful DAG upload + if subscriberStore, ok := store.(stores.SubscriberStore); ok { + dagJson, err := dagData.Dag.ToJSON() // Get actual size after DAG is built + if err != nil { + log.Printf("Warning: failed to marshall dag to json: %s", err) + } + actualSize := int64(len(dagJson)) + if err := subscriberStore.UpdateStorageUsage(message.PublicKey, actualSize); err != nil { + log.Printf("Warning: Failed to update storage usage for %s: %v", message.PublicKey, err) + // Continue despite tracking failure as DAG is already stored + } + + // Track the upload + upload := &types.FileUpload{ + Npub: message.PublicKey, + FileHash: message.Root, + SizeBytes: actualSize, + } + if err := subscriberStore.TrackFileUpload(upload); err != nil { + log.Printf("Warning: Failed to track file upload for %s: %v", message.PublicKey, err) + } + } + handleRecievedDag(&dagData.Dag, &message.PublicKey) } diff --git a/lib/stores/graviton/graviton.go b/lib/stores/graviton/graviton.go index 0b8460e..fd39be0 100644 --- a/lib/stores/graviton/graviton.go +++ b/lib/stores/graviton/graviton.go @@ -20,6 +20,7 @@ import ( stores "github.com/HORNET-Storage/hornet-storage/lib/stores" gorm "github.com/HORNET-Storage/hornet-storage/lib/stores/stats_stores" + subscriber_store "github.com/HORNET-Storage/hornet-storage/lib/stores/subscription_store" merkle_dag "github.com/HORNET-Storage/scionic-merkletree/dag" jsoniter "github.com/json-iterator/go" @@ -34,39 +35,50 @@ const ( ) type GravitonStore struct { - Database *graviton.Store - StatsDatabase stores.StatisticsStore - mu sync.Mutex + Database *graviton.Store + StatsDatabase stores.StatisticsStore + SubscriberStore stores.SubscriberStore + mu sync.Mutex } func (store *GravitonStore) InitStore(basepath string, args ...interface{}) error { db, err := graviton.NewDiskStore(basepath) if err != nil { - return err + return fmt.Errorf("failed to initialize Graviton DB: %v", err) } // Initialize GORM StatsDatabase store.StatsDatabase = &gorm.GormStatisticsStore{} - err = store.StatsDatabase.InitStore(viper.GetString("relay_stats_db"), nil) - if err != nil { - return fmt.Errorf("failed to initialize StatsDatabase: %v", err) // Proper error handling + if err = store.StatsDatabase.InitStore(viper.GetString("relay_stats_db"), nil); err != nil { + return fmt.Errorf("failed to initialize StatsDatabase: %v", err) + } + + // Initialize SubscriberStore + store.SubscriberStore = &subscriber_store.GormSubscriberStore{} + if err = store.SubscriberStore.InitStore(viper.GetString("subscriber_db"), nil); err != nil { + return fmt.Errorf("failed to initialize SubscriberStore: %v", err) + } + if store.SubscriberStore == nil { + log.Println("Warning: SubscriberStore not initialized correctly.") + } else { + log.Println("SubscriberStore initialized successfully.") } store.Database = db snapshot, err := db.LoadSnapshot(0) if err != nil { - return err + return fmt.Errorf("failed to load snapshot: %v", err) } tree, err := snapshot.GetTree("content") if err != nil { - return err + return fmt.Errorf("failed to get content tree: %v", err) } _, err = graviton.Commit(tree) if err != nil { - return err + return fmt.Errorf("failed to commit tree: %v", err) } return nil @@ -76,6 +88,10 @@ func (store *GravitonStore) GetStatsStore() stores.StatisticsStore { return store.StatsDatabase } +func (store *GravitonStore) GetSubscriberStore() stores.SubscriberStore { + return store.SubscriberStore +} + // Scionic Merkletree's (Chunked data) // Query scionic merkletree's by providing a key and a value, key being the bucket and value being the key of how the tree is cached // An example would be "npub:app": "filetype" because the trees are cached in buckets per user per app and filetypes @@ -659,29 +675,82 @@ func (store *GravitonStore) CountFileLeavesByType() (map[string]int, error) { } // Blossom Blobs (unchunked data) +// StoreBlob saves the blob data and updates storage tracking +// Parameters: +// - data: The actual file data to store +// - hash: The SHA-256 hash of the data +// - publicKey: The subscriber's public key func (store *GravitonStore) StoreBlob(data []byte, hash []byte, publicKey string) error { + store.mu.Lock() + defer store.mu.Unlock() + + // Check if storage tracking is available + if store.SubscriberStore != nil { + // Pre-check storage availability + fileSize := int64(len(data)) + if err := store.SubscriberStore.CheckStorageAvailability(publicKey, fileSize); err != nil { + return fmt.Errorf("storage quota check failed: %v", err) + } + } + + // Load snapshot and get content tree snapshot, err := store.Database.LoadSnapshot(0) if err != nil { - return err + return fmt.Errorf("failed to load snapshot: %v", err) } contentTree, err := snapshot.GetTree("content") if err != nil { - return err + return fmt.Errorf("failed to get content tree: %v", err) } + // Generate hash string and cache keys encodedHash := hex.EncodeToString(hash[:]) - cacheTrees, err := store.cacheKey(publicKey, "blossom", encodedHash) if err != nil { - return err + return fmt.Errorf("failed to generate cache keys: %v", err) } - contentTree.Put(hash[:], data) + // Store the actual data + if err := contentTree.Put(hash[:], data); err != nil { + return fmt.Errorf("failed to store content: %v", err) + } + // Prepare all trees for commit cacheTrees = append(cacheTrees, contentTree) - graviton.Commit(cacheTrees...) + // Commit the data + if _, err := graviton.Commit(cacheTrees...); err != nil { + return fmt.Errorf("failed to commit trees: %v", err) + } + + // Update storage tracking if available + if store.SubscriberStore != nil { + fileSize := int64(len(data)) + + // Track the file upload + upload := &types.FileUpload{ + Npub: publicKey, + FileHash: encodedHash, + SizeBytes: fileSize, + } + + if err := store.SubscriberStore.TrackFileUpload(upload); err != nil { + log.Printf("Warning: Failed to track file upload for %s: %v", publicKey, err) + // Continue despite tracking failure as data is already stored + } + + // Update storage usage + if err := store.SubscriberStore.UpdateStorageUsage(publicKey, fileSize); err != nil { + log.Printf("Warning: Failed to update storage usage for %s: %v", publicKey, err) + } + + // Log successful upload with storage stats + if stats, err := store.SubscriberStore.GetSubscriberStorageStats(publicKey); err == nil { + log.Printf("Blob stored successfully for %s: size=%d bytes, total_used=%d bytes (%.2f%% of quota)", + publicKey, fileSize, stats.CurrentUsageBytes, stats.UsagePercentage) + } + } return nil } @@ -1014,6 +1083,12 @@ func ContainsAny(tags nostr.Tags, tagName string, values []string) bool { func (store *GravitonStore) SaveSubscriber(subscriber *types.Subscriber) error { store.mu.Lock() defer store.mu.Unlock() + + // Save to GORM first + if err := store.SubscriberStore.SaveSubscriber(subscriber); err != nil { + return fmt.Errorf("failed to save subscriber to GORM: %v", err) + } + // Load the snapshot and get the "subscribers" tree snapshot, err := store.Database.LoadSnapshot(0) if err != nil { @@ -1048,6 +1123,23 @@ func (store *GravitonStore) SaveSubscriber(subscriber *types.Subscriber) error { } func (store *GravitonStore) GetSubscriberByAddress(address string) (*types.Subscriber, error) { + + // Try GORM store first + if store.SubscriberStore != nil { + if subscriberStore, ok := store.SubscriberStore.(interface { + GetSubscriberByAddress(address string) (*types.Subscriber, error) + }); ok { + subscriber, err := subscriberStore.GetSubscriberByAddress(address) + if err == nil { + return subscriber, nil + } + // Only log the error if it's not a "not found" error + if !strings.Contains(err.Error(), "not found") { + log.Printf("GORM lookup failed: %v, falling back to Graviton", err) + } + } + } + store.mu.Lock() defer store.mu.Unlock() @@ -1116,6 +1208,41 @@ func (store *GravitonStore) GetSubscriber(npub string) (*types.Subscriber, error return nil, fmt.Errorf("subscriber not found for npub: %s", npub) } +// DeleteSubscriber removes a subscriber from both the Graviton store and GORM store +func (store *GravitonStore) DeleteSubscriber(npub string) error { + store.mu.Lock() + defer store.mu.Unlock() + + // First try to delete from GORM store if available + if store.SubscriberStore != nil { + if err := store.SubscriberStore.DeleteSubscriber(npub); err != nil { + // Log the error but continue with Graviton deletion + log.Printf("Warning: Failed to delete subscriber from GORM store: %v", err) + } + } + + snapshot, err := store.Database.LoadSnapshot(0) + if err != nil { + return fmt.Errorf("failed to load snapshot: %v", err) + } + + tree, err := snapshot.GetTree("subscribers") + if err == nil { + err := tree.Delete([]byte(npub)) + if err != nil { + log.Printf("Error during subscriber deletion: %v", err) + return fmt.Errorf("failed to delete subscriber: %v", err) + } else { + log.Printf("Deleted subscriber %s", npub) + } + } + + // Following DeleteEvent pattern: directly commit the tree without checking result + graviton.Commit(tree) + + return nil +} + // AllocateBitcoinAddress allocates an available Bitcoin address to a subscriber. func (store *GravitonStore) AllocateBitcoinAddress(npub string) (*types.Address, error) { // Load snapshot from the database diff --git a/lib/stores/memory/memory.go b/lib/stores/memory/memory.go index 27f336c..bce999c 100644 --- a/lib/stores/memory/memory.go +++ b/lib/stores/memory/memory.go @@ -72,6 +72,35 @@ func (store *GravitonMemoryStore) GetStatsStore() stores.StatisticsStore { return nil } +// Not implemented for the Memory Store +func (store *GravitonMemoryStore) GetSubscriberStore() stores.SubscriberStore { + return nil +} + +// Not implemented for the Memory Store +func (store *GravitonMemoryStore) DeleteSubscriber(npub string) error { + snapshot, err := store.Database.LoadSnapshot(0) + if err != nil { + return fmt.Errorf("failed to load snapshot: %v", err) + } + + tree, err := snapshot.GetTree("subscribers") + if err == nil { + err := tree.Delete([]byte(npub)) + if err != nil { + log.Printf("Error during subscriber deletion: %v", err) + return fmt.Errorf("failed to delete subscriber: %v", err) + } else { + log.Printf("Deleted subscriber %s", npub) + } + } + + // Following DeleteEvent pattern: directly commit the tree without checking result + graviton.Commit(tree) + + return nil +} + func (store *GravitonMemoryStore) QueryDag(filter map[string]string) ([]string, error) { keys := []string{} diff --git a/lib/stores/stats_stores/statistics_store_gorm.go b/lib/stores/stats_stores/statistics_store_gorm.go index cbc1476..3a8193c 100644 --- a/lib/stores/stats_stores/statistics_store_gorm.go +++ b/lib/stores/stats_stores/statistics_store_gorm.go @@ -559,7 +559,6 @@ func (store *GormStatisticsStore) ReplaceTransaction(replaceRequest types.Replac var originalPendingTransaction types.PendingTransaction if err := store.DB.Where("tx_id = ?", replaceRequest.OriginalTxID).First(&originalPendingTransaction).Error; err != nil { if err == gorm.ErrRecordNotFound { - log.Printf("No pending transaction found with TxID %s", replaceRequest.OriginalTxID) return gorm.ErrRecordNotFound } log.Printf("Error querying original transaction with TxID %s: %v", replaceRequest.OriginalTxID, err) @@ -865,7 +864,7 @@ func (store *GormStatisticsStore) AllocateBitcoinAddress(npub string) (*types.Ad WalletName: subscriptionAddress.WalletName, Status: subscriptionAddress.Status, AllocatedAt: subscriptionAddress.AllocatedAt, - Npub: subscriptionAddress.Npub, + Npub: npub, }, nil } diff --git a/lib/stores/stores.go b/lib/stores/stores.go index 751111c..4b61cc0 100644 --- a/lib/stores/stores.go +++ b/lib/stores/stores.go @@ -14,6 +14,9 @@ type Store interface { // Statistics Store GetStatsStore() StatisticsStore + // Subscriber Store + GetSubscriberStore() SubscriberStore + // Hornet Storage StoreLeaf(root string, leafData *types.DagLeafData) error RetrieveLeaf(root string, hash string, includeContent bool) (*types.DagLeafData, error) @@ -39,6 +42,7 @@ type Store interface { SaveSubscriber(subscriber *types.Subscriber) error AllocateBitcoinAddress(npub string) (*types.Address, error) SaveAddress(addr *types.Address) error + DeleteSubscriber(npub string) error } func BuildDagFromStore(store Store, root string, includeContent bool) (*types.DagData, error) { diff --git a/lib/stores/subscriber_store.go b/lib/stores/subscriber_store.go new file mode 100644 index 0000000..61e3f28 --- /dev/null +++ b/lib/stores/subscriber_store.go @@ -0,0 +1,60 @@ +package stores + +import ( + "fmt" + + types "github.com/HORNET-Storage/hornet-storage/lib" +) + +// SubscriberStore defines the interface for managing subscribers and their storage usage +type SubscriberStore interface { + // Store initialization + InitStore(basepath string, args ...interface{}) error + + // Subscriber management + SaveSubscriber(subscriber *types.Subscriber) error + GetSubscriber(npub string) (*types.Subscriber, error) + GetSubscriberByAddress(address string) (*types.Subscriber, error) + DeleteSubscriber(npub string) error + ListSubscribers() ([]*types.Subscriber, error) + + // Storage quota and statistics + GetSubscriberStorageStats(npub string) (*types.StorageStats, error) + UpdateStorageUsage(npub string, sizeBytes int64) error + GetStorageUsage(npub string) (*types.StorageUsage, error) + CheckStorageAvailability(npub string, requestedBytes int64) error + + // Subscription periods + AddSubscriptionPeriod(npub string, period *types.SubscriptionPeriod) error + GetSubscriptionPeriods(npub string) ([]*types.SubscriptionPeriod, error) + GetActiveSubscription(npub string) (*types.SubscriptionPeriod, error) + GetSubscriptionByTransactionID(transactionID string) (*types.SubscriptionPeriod, error) + + // File management + TrackFileUpload(upload *types.FileUpload) error + DeleteFile(npub string, fileHash string) error + GetFilesBySubscriber(npub string) ([]*types.FileUpload, error) + GetRecentUploads(npub string, limit int) ([]*types.FileUpload, error) + + // Address management + SaveSubscriberAddress(address *types.SubscriberAddress) error + AllocateBitcoinAddress(npub string) (*types.Address, error) + AddressExists(address string) (bool, error) + SaveSubscriberAddresses(address *types.WalletAddress) error +} + +// Convert storage strings to bytes +func ParseStorageLimit(limit string) (int64, error) { + var bytes int64 + switch limit { + case "1 GB per month": + bytes = 1 * 1024 * 1024 * 1024 + case "5 GB per month": + bytes = 5 * 1024 * 1024 * 1024 + case "10 GB per month": + bytes = 10 * 1024 * 1024 * 1024 + default: + return 0, fmt.Errorf("unknown storage limit: %s", limit) + } + return bytes, nil +} diff --git a/lib/stores/subscription_store/subscription_store_gorm.go b/lib/stores/subscription_store/subscription_store_gorm.go new file mode 100644 index 0000000..3186c81 --- /dev/null +++ b/lib/stores/subscription_store/subscription_store_gorm.go @@ -0,0 +1,646 @@ +package subscription_gorm + +import ( + "fmt" + "log" + "time" + + types "github.com/HORNET-Storage/hornet-storage/lib" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +type GormSubscriberStore struct { + DB *gorm.DB +} + +const ( + AddressStatusAvailable = "available" + AddressStatusAllocated = "allocated" + AddressStatusUsed = "used" +) + +// Convert storage strings to bytes +func parseStorageLimit(limit string) (int64, error) { + var bytes int64 + switch limit { + case "1 GB per month": + bytes = 1 * 1024 * 1024 * 1024 + case "5 GB per month": + bytes = 5 * 1024 * 1024 * 1024 + case "10 GB per month": + bytes = 10 * 1024 * 1024 * 1024 + default: + return 0, fmt.Errorf("unknown storage limit: %s", limit) + } + return bytes, nil +} + +// InitStore initializes the GORM subscriber store +func (store *GormSubscriberStore) InitStore(basepath string, args ...interface{}) error { + var err error + + // Initialize the database connection + store.DB, err = gorm.Open(sqlite.Open(basepath), &gorm.Config{}) + if err != nil { + return fmt.Errorf("failed to connect to database: %v", err) + } + + // Run migrations + err = store.DB.AutoMigrate( + &types.GormSubscriber{}, + &types.SubscriptionPeriod{}, + &types.FileUpload{}, + &types.SubscriberAddress{}, + &types.WalletAddress{}, + ) + if err != nil { + return fmt.Errorf("failed to run migrations: %v", err) + } + + return nil +} + +// NewGormSubscriberStore creates a new instance of GormSubscriberStore +func NewGormSubscriberStore() *GormSubscriberStore { + return &GormSubscriberStore{} +} + +// SaveSubscriber saves or updates a subscriber +// For new subscribers: +// - Creates with default values (no tier/storage) +// For existing subscribers: +// - Updates with new tier and storage limits if provided +func (store *GormSubscriberStore) SaveSubscriber(subscriber *types.Subscriber) error { + tx := store.DB.Begin() + if tx.Error != nil { + return tx.Error + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + var gormSubscriber types.GormSubscriber + result := tx.Where("npub = ?", subscriber.Npub).First(&gormSubscriber) + + if result.Error == gorm.ErrRecordNotFound { + // Creating new subscriber + gormSubscriber = types.GormSubscriber{ + Npub: subscriber.Npub, + StorageUsedBytes: 0, + StorageLimitBytes: 0, // No storage limit until subscription + LastUpdated: time.Now(), + } + + // If tier is provided (unusual for new subscriber but possible) + if subscriber.Tier != "" { + storageLimitBytes, err := parseStorageLimit(subscriber.Tier) + if err != nil { + tx.Rollback() + return fmt.Errorf("invalid tier for new subscriber: %v", err) + } + gormSubscriber.CurrentTier = subscriber.Tier + gormSubscriber.StorageLimitBytes = storageLimitBytes + gormSubscriber.StartDate = subscriber.StartDate + gormSubscriber.EndDate = subscriber.EndDate + } + + if err := tx.Create(&gormSubscriber).Error; err != nil { + tx.Rollback() + return fmt.Errorf("failed to create subscriber: %v", err) + } + + log.Printf("Created new subscriber: %s", subscriber.Npub) + } else if result.Error != nil { + tx.Rollback() + return fmt.Errorf("failed to query subscriber: %v", result.Error) + } else { + // Updating existing subscriber + if subscriber.Tier != "" { + // Only update tier-related fields if a tier is provided + storageLimitBytes, err := parseStorageLimit(subscriber.Tier) + if err != nil { + tx.Rollback() + return fmt.Errorf("invalid tier for subscription update: %v", err) + } + + gormSubscriber.CurrentTier = subscriber.Tier + gormSubscriber.StorageLimitBytes = storageLimitBytes + gormSubscriber.StartDate = subscriber.StartDate + gormSubscriber.EndDate = subscriber.EndDate + } + + gormSubscriber.LastUpdated = time.Now() + + if err := tx.Save(&gormSubscriber).Error; err != nil { + tx.Rollback() + return fmt.Errorf("failed to update subscriber: %v", err) + } + + log.Printf("Updated subscriber: %s", subscriber.Npub) + } + + // Only create subscription period if tier and transaction ID are provided + if subscriber.Tier != "" && subscriber.LastTransactionID != "" { + storageLimitBytes, _ := parseStorageLimit(subscriber.Tier) // Error already checked above + subscriptionPeriod := types.SubscriptionPeriod{ + SubscriberID: gormSubscriber.ID, + TransactionID: subscriber.LastTransactionID, + Tier: subscriber.Tier, + StorageLimitBytes: storageLimitBytes, + StartDate: subscriber.StartDate, + EndDate: subscriber.EndDate, + PaymentAmount: "", // Add payment amount when available + } + + if err := tx.Create(&subscriptionPeriod).Error; err != nil { + tx.Rollback() + return fmt.Errorf("failed to create subscription period: %v", err) + } + + log.Printf("Created subscription period for %s: %s tier", + subscriber.Npub, subscriber.Tier) + } + + return tx.Commit().Error +} + +// GetSubscriberStorageStats gets detailed storage statistics +func (store *GormSubscriberStore) GetSubscriberStorageStats(npub string) (*types.StorageStats, error) { + var subscriber types.GormSubscriber + if err := store.DB.Where("npub = ?", npub).First(&subscriber).Error; err != nil { + return nil, err + } + + var recentFiles []types.FileUpload + if err := store.DB.Where("npub = ? AND deleted = ?", npub, false). + Order("created_at desc"). + Limit(10). + Find(&recentFiles).Error; err != nil { + return nil, err + } + + return &types.StorageStats{ + CurrentUsageBytes: subscriber.StorageUsedBytes, + StorageLimitBytes: subscriber.StorageLimitBytes, + UsagePercentage: float64(subscriber.StorageUsedBytes) / float64(subscriber.StorageLimitBytes) * 100, + SubscriptionEnd: subscriber.EndDate, + CurrentTier: subscriber.CurrentTier, + LastUpdated: subscriber.LastUpdated, + RecentFiles: recentFiles, + }, nil +} + +// UpdateStorageUsage updates a subscriber's storage usage +func (store *GormSubscriberStore) UpdateStorageUsage(npub string, sizeBytes int64) error { + tx := store.DB.Begin() + if tx.Error != nil { + return tx.Error + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + var subscriber types.GormSubscriber + if err := tx.Where("npub = ?", npub).First(&subscriber).Error; err != nil { + tx.Rollback() + return err + } + + newUsage := subscriber.StorageUsedBytes + sizeBytes + if newUsage > subscriber.StorageLimitBytes { + tx.Rollback() + return fmt.Errorf("storage limit exceeded: would use %d of %d bytes", newUsage, subscriber.StorageLimitBytes) + } + + if err := tx.Model(&subscriber).Updates(map[string]interface{}{ + "storage_used_bytes": newUsage, + "last_updated": time.Now(), + }).Error; err != nil { + tx.Rollback() + return err + } + + return tx.Commit().Error +} + +// TrackFileUpload records a new file upload +func (store *GormSubscriberStore) TrackFileUpload(upload *types.FileUpload) error { + return store.DB.Create(upload).Error +} + +// GetSubscriber retrieves a subscriber by npub +func (store *GormSubscriberStore) GetSubscriber(npub string) (*types.Subscriber, error) { + var gormSubscriber types.GormSubscriber + if err := store.DB.Where("npub = ?", npub).First(&gormSubscriber).Error; err != nil { + if err == gorm.ErrRecordNotFound { + // Create a new subscriber with just the npub + newSubscriber := &types.Subscriber{ + Npub: npub, + } + if err := store.SaveSubscriber(newSubscriber); err != nil { + return nil, fmt.Errorf("failed to create new subscriber: %v", err) + } + return newSubscriber, nil + } + return nil, err + } + return gormSubscriber.ToSubscriber(), nil +} + +// DeleteSubscriber removes a subscriber +func (store *GormSubscriberStore) DeleteSubscriber(npub string) error { + return store.DB.Where("npub = ?", npub).Delete(&types.GormSubscriber{}).Error +} + +// ListSubscribers returns all subscribers +func (store *GormSubscriberStore) ListSubscribers() ([]*types.Subscriber, error) { + var gormSubscribers []types.GormSubscriber + if err := store.DB.Find(&gormSubscribers).Error; err != nil { + return nil, err + } + + subscribers := make([]*types.Subscriber, len(gormSubscribers)) + for i, gs := range gormSubscribers { + subscribers[i] = gs.ToSubscriber() + } + return subscribers, nil +} + +// AddSubscriptionPeriod adds a new subscription period +func (store *GormSubscriberStore) AddSubscriptionPeriod(npub string, period *types.SubscriptionPeriod) error { + var subscriber types.GormSubscriber + if err := store.DB.Where("npub = ?", npub).First(&subscriber).Error; err != nil { + return err + } + + period.SubscriberID = subscriber.ID + return store.DB.Create(period).Error +} + +// GetSubscriptionPeriods retrieves all subscription periods for a subscriber +func (store *GormSubscriberStore) GetSubscriptionPeriods(npub string) ([]*types.SubscriptionPeriod, error) { + var subscriber types.GormSubscriber + if err := store.DB.Where("npub = ?", npub).First(&subscriber).Error; err != nil { + return nil, err + } + + var periods []*types.SubscriptionPeriod + if err := store.DB.Where("subscriber_id = ?", subscriber.ID). + Order("start_date desc"). + Find(&periods).Error; err != nil { + return nil, err + } + + return periods, nil +} + +// GetActiveSubscription gets the current active subscription +func (store *GormSubscriberStore) GetActiveSubscription(npub string) (*types.SubscriptionPeriod, error) { + var subscriber types.GormSubscriber + if err := store.DB.Where("npub = ?", npub).First(&subscriber).Error; err != nil { + return nil, err + } + + var period types.SubscriptionPeriod + if err := store.DB.Where("subscriber_id = ? AND end_date > ?", subscriber.ID, time.Now()). + Order("end_date desc"). + First(&period).Error; err != nil { + return nil, err + } + + return &period, nil +} + +func (store *GormSubscriberStore) GetSubscriberByAddress(address string) (*types.Subscriber, error) { + // First find the subscriber_address record with explicit column selection + var subscriberAddress struct { + Address string + Status string + Npub string + } + + err := store.DB.Table("subscriber_addresses"). + Select("address, status, npub"). + Where("address = ?", address). + First(&subscriberAddress).Error + + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("address %s not allocated to any subscriber", address) + } + return nil, fmt.Errorf("error querying address: %v", err) + } + + log.Printf("Found subscriber address record: address=%s, npub=%s, status=%s", + subscriberAddress.Address, subscriberAddress.Npub, subscriberAddress.Status) + + // Verify we have a valid npub + if subscriberAddress.Npub == "" { + return nil, fmt.Errorf("address %s has no associated npub", address) + } + + log.Println("Gorm Subscriber Npub: ", subscriberAddress.Npub) + + // Now get the subscriber using the npub + var subscriber types.GormSubscriber + if err := store.DB.Where("npub = ?", subscriberAddress.Npub).First(&subscriber).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("subscriber not found for npub: %s", subscriberAddress.Npub) + } + return nil, fmt.Errorf("error querying subscriber: %v", err) + } + + // Create and return the subscriber + return &types.Subscriber{ + Npub: subscriber.Npub, + Tier: subscriber.CurrentTier, + StartDate: subscriber.StartDate, + EndDate: subscriber.EndDate, + Address: address, + }, nil +} + +func (store *GormSubscriberStore) SaveSubscriberAddresses(address *types.WalletAddress) error { + return store.DB.Create(address).Error +} + +func (store *GormSubscriberStore) SaveSubscriberAddress(address *types.SubscriberAddress) error { + // Check if the address already exists + var existingAddress types.SubscriberAddress + result := store.DB.Where("address = ?", address.Address).First(&existingAddress) + + if result.Error != nil && result.Error != gorm.ErrRecordNotFound { + log.Printf("Error querying existing address: %v", result.Error) + return result.Error + } + + // If the address already exists, log and skip the insert + if result.RowsAffected > 0 { + log.Printf("Address %s already exists, skipping save.", address.Address) + return nil + } + + // Set defaults if needed + if address.Status == "" { + address.Status = AddressStatusAvailable + } + if address.AllocatedAt == nil { + now := time.Now() + address.AllocatedAt = &now + } + address.Npub = "" // Explicitly set to NULL + + // Create the new address in the database + if err := store.DB.Create(address).Error; err != nil { + log.Printf("Error saving new address: %v", err) + return err + } + + log.Printf("Address %s saved successfully.", address.Address) + return nil +} + +func (store *GormSubscriberStore) AllocateBitcoinAddress(npub string) (*types.Address, error) { + tx := store.DB.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + // Modified query to handle NULL npub + var subscriptionAddress types.SubscriberAddress + err := tx.Where("status = ? AND (npub IS NULL OR npub = '')", AddressStatusAvailable). + Order("id"). + First(&subscriptionAddress).Error + + if err != nil { + tx.Rollback() + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("no available addresses") + } + return nil, fmt.Errorf("failed to query available addresses: %v", err) + } + + // Update the address fields + now := time.Now() + updates := map[string]interface{}{ + "status": AddressStatusAllocated, + "allocated_at": &now, + "npub": npub, + } + + if err := tx.Model(&subscriptionAddress).Updates(updates).Error; err != nil { + tx.Rollback() + return nil, fmt.Errorf("failed to update address allocation: %v", err) + } + + if err := tx.Commit().Error; err != nil { + return nil, fmt.Errorf("failed to commit transaction: %v", err) + } + + return &types.Address{ + Index: subscriptionAddress.Index, + Address: subscriptionAddress.Address, + WalletName: subscriptionAddress.WalletName, + Status: subscriptionAddress.Status, + AllocatedAt: subscriptionAddress.AllocatedAt, + Npub: npub, + }, nil +} + +func (store *GormSubscriberStore) AddressExists(address string) (bool, error) { + var count int64 + err := store.DB.Model(&types.SubscriberAddress{}). + Where("address = ?", address). + Count(&count).Error + + if err != nil { + return false, fmt.Errorf("failed to check address existence: %v", err) + } + + return count > 0, nil +} + +// GetSubscriptionByTransactionID retrieves a subscription by its transaction ID +func (store *GormSubscriberStore) GetSubscriptionByTransactionID(transactionID string) (*types.SubscriptionPeriod, error) { + var period types.SubscriptionPeriod + if err := store.DB.Where("transaction_id = ?", transactionID).First(&period).Error; err != nil { + return nil, err + } + return &period, nil +} + +// CheckStorageAvailability verifies if the subscriber has enough storage space +func (store *GormSubscriberStore) CheckStorageAvailability(npub string, requestedBytes int64) error { + var subscriber types.GormSubscriber + if err := store.DB.Where("npub = ?", npub).First(&subscriber).Error; err != nil { + return err + } + + // If no tier/storage limit is set, they can't upload + if subscriber.StorageLimitBytes == 0 { + return fmt.Errorf("no active subscription: storage limit not set") + } + + if subscriber.EndDate.IsZero() || time.Now().After(subscriber.EndDate) { + return fmt.Errorf("subscription expired or not yet activated") + } + + newUsage := subscriber.StorageUsedBytes + requestedBytes + if newUsage > subscriber.StorageLimitBytes { + return fmt.Errorf("storage limit exceeded: would use %d of %d bytes", + newUsage, subscriber.StorageLimitBytes) + } + + return nil +} + +// GetStorageUsage gets current storage usage +func (store *GormSubscriberStore) GetStorageUsage(npub string) (*types.StorageUsage, error) { + var subscriber types.GormSubscriber + if err := store.DB.Where("npub = ?", npub).First(&subscriber).Error; err != nil { + return nil, err + } + + return &types.StorageUsage{ + Npub: subscriber.Npub, + UsedBytes: subscriber.StorageUsedBytes, + AllocatedBytes: subscriber.StorageLimitBytes, + LastUpdated: subscriber.LastUpdated, + }, nil +} + +// GetFilesBySubscriber retrieves all files for a subscriber +func (store *GormSubscriberStore) GetFilesBySubscriber(npub string) ([]*types.FileUpload, error) { + var files []*types.FileUpload + if err := store.DB.Where("npub = ? AND deleted = ?", npub, false). + Order("created_at desc"). + Find(&files).Error; err != nil { + return nil, err + } + return files, nil +} + +// GetRecentUploads gets the most recent uploads for a subscriber +func (store *GormSubscriberStore) GetRecentUploads(npub string, limit int) ([]*types.FileUpload, error) { + var files []*types.FileUpload + if err := store.DB.Where("npub = ? AND deleted = ?", npub, false). + Order("created_at desc"). + Limit(limit). + Find(&files).Error; err != nil { + return nil, err + } + return files, nil +} + +// DeleteFile marks a file as deleted and updates storage usage +func (store *GormSubscriberStore) DeleteFile(npub string, fileHash string) error { + tx := store.DB.Begin() + if tx.Error != nil { + return tx.Error + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + var file types.FileUpload + if err := tx.Where("npub = ? AND file_hash = ? AND deleted = ?", npub, fileHash, false). + First(&file).Error; err != nil { + tx.Rollback() + return err + } + + now := time.Now() + if err := tx.Model(&file).Updates(map[string]interface{}{ + "deleted": true, + "deleted_at": &now, + }).Error; err != nil { + tx.Rollback() + return err + } + + if err := tx.Model(&types.GormSubscriber{}). + Where("npub = ?", npub). + UpdateColumn("storage_used_bytes", gorm.Expr("storage_used_bytes - ?", file.SizeBytes)). + Error; err != nil { + tx.Rollback() + return err + } + + return tx.Commit().Error +} + +func (store *GormSubscriberStore) DebugAddressDetails(address string) { + var addr types.SubscriberAddress + result := store.DB.Where("address = ?", address).First(&addr) + + if result.Error != nil { + log.Printf("Error querying address details: %v", result.Error) + return + } + + log.Printf("=== Address Details ===") + log.Printf("Address: %s", addr.Address) + log.Printf("Status: %s", addr.Status) + if addr.Npub != "" { + log.Printf("Allocated to npub: %s", addr.Npub) + } else { + log.Printf("Not allocated to any npub") + } + if addr.AllocatedAt != nil { + log.Printf("Allocated at: %v", *addr.AllocatedAt) + } + log.Printf("=====================") +} + +func (store *GormSubscriberStore) DumpAddressTable() { + var addresses []types.SubscriberAddress + result := store.DB.Find(&addresses) + + if result.Error != nil { + log.Printf("Error querying address table: %v", result.Error) + return + } + + log.Printf("=== Address Table Contents ===") + log.Printf("Found %d addresses", len(addresses)) + for _, addr := range addresses { + log.Printf("Address: %s, Status: %s, Npub: %v", + addr.Address, + addr.Status, + addr.Npub) + } + log.Printf("============================") +} + +// Add this debug select to check the address in the database directly +func (store *GormSubscriberStore) DebugAddressTableContent() { + var results []struct { + Address string + Status string + Npub *string + } + + store.DB.Raw(` + SELECT address, status, npub + FROM subscriber_addresses + WHERE status = 'allocated' + ORDER BY allocated_at DESC + LIMIT 5 + `).Scan(&results) + + log.Printf("=== Recent Allocated Addresses ===") + for _, r := range results { + log.Printf("Address: %s, Status: %s, Npub: %v", + r.Address, r.Status, r.Npub) + } + log.Printf("=================================") +} diff --git a/lib/subscription/subscription.go b/lib/subscription/subscription.go new file mode 100644 index 0000000..a26c275 --- /dev/null +++ b/lib/subscription/subscription.go @@ -0,0 +1,837 @@ +// subscription.go + +package subscription + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "log" + "strconv" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/nbd-wtf/go-nostr" + + "github.com/HORNET-Storage/hornet-storage/lib" + "github.com/HORNET-Storage/hornet-storage/lib/stores" +) + +// StorageInfo tracks current storage usage information for a subscriber +type StorageInfo struct { + UsedBytes int64 // Current bytes used by the subscriber + TotalBytes int64 // Total bytes allocated to the subscriber + UpdatedAt time.Time // Last time storage information was updated +} + +// SubscriptionManager handles all subscription-related operations including: +// - Subscriber management +// - NIP-88 event creation and updates +// - Storage tracking +// - Payment processing +type SubscriptionManager struct { + store stores.Store // Interface to the storage layer + relayPrivateKey *btcec.PrivateKey // Relay's private key for signing events + // relayBTCAddress string // Relay's Bitcoin address for payments + relayDHTKey string // Relay's DHT key + subscriptionTiers []lib.SubscriptionTier // Available subscription tiers +} + +// NewSubscriptionManager creates and initializes a new subscription manager +func NewSubscriptionManager( + store stores.Store, + relayPrivKey *btcec.PrivateKey, + // relayBTCAddress string, + relayDHTKey string, + tiers []lib.SubscriptionTier, +) *SubscriptionManager { + return &SubscriptionManager{ + store: store, + relayPrivateKey: relayPrivKey, + // relayBTCAddress: relayBTCAddress, + relayDHTKey: relayDHTKey, + subscriptionTiers: tiers, + } +} + +// InitializeSubscriber creates a new subscriber or retrieves an existing one +// and creates their initial NIP-88 event. This is called when a user first +// connects to the relay. +func (m *SubscriptionManager) InitializeSubscriber(npub string) error { + // Step 1: Get or create subscriber record + subscriber, err := m.getOrCreateSubscriber(npub) + if err != nil { + return fmt.Errorf("failed to initialize subscriber: %v", err) + } + + // Step 2: Create initial NIP-88 event with zero storage usage + storageInfo := StorageInfo{ + UsedBytes: 0, + TotalBytes: 0, + UpdatedAt: time.Now(), + } + + // Step 3: Create the NIP-88 event + return m.createOrUpdateNIP88Event(subscriber, "", time.Time{}, &storageInfo) +} + +// ProcessPayment handles a new subscription payment. It: +// - Validates the payment amount against available tiers +// - Updates subscriber information +// - Creates a new subscription period +// - Updates the NIP-88 event +func (m *SubscriptionManager) ProcessPayment( + npub string, + transactionID string, + amountSats int64, +) error { + // Step 1: Find matching tier for payment amount + tier, err := m.findMatchingTier(amountSats) + if err != nil { + return fmt.Errorf("error matching tier: %v", err) + } + + // Step 2: Get existing subscriber + subscriber, err := m.store.GetSubscriber(npub) + if err != nil { + return fmt.Errorf("subscriber not found: %v", err) + } + + // Step 3: Verify transaction hasn't been processed before + existingPeriod, err := m.store.GetSubscriberStore().GetSubscriptionByTransactionID(transactionID) + if err == nil && existingPeriod != nil { + return fmt.Errorf("transaction %s already processed", transactionID) + } + + // Step 4: Calculate subscription period dates + startDate := time.Now() + endDate := m.calculateEndDate(subscriber.EndDate) + storageLimit := m.calculateStorageLimit(tier.DataLimit) + + // Step 5: Initialize storage tracking + storageInfo := StorageInfo{ + UsedBytes: 0, // Reset for new subscription + TotalBytes: storageLimit, + UpdatedAt: time.Now(), + } + + log.Printf("Updating NIP-88 event for subscriber %s with tier %s", npub, tier.DataLimit) + // Step 6: Update NIP-88 event + if err := m.createOrUpdateNIP88Event(subscriber, tier.DataLimit, endDate, &storageInfo); err != nil { + log.Printf("Error updating NIP-88 event: %v", err) + } + + // Step 7: Create subscription period record + period := &lib.SubscriptionPeriod{ + TransactionID: transactionID, + Tier: tier.DataLimit, + StorageLimitBytes: storageLimit, + StartDate: startDate, + EndDate: endDate, + PaymentAmount: fmt.Sprintf("%d", amountSats), + } + + if err := m.store.GetSubscriberStore().AddSubscriptionPeriod(npub, period); err != nil { + return fmt.Errorf("failed to add subscription period: %v", err) + } + + // Step 8: Update subscriber record + subscriber.Tier = tier.DataLimit + subscriber.StartDate = startDate + subscriber.EndDate = endDate + subscriber.LastTransactionID = transactionID + + if err := m.store.SaveSubscriber(subscriber); err != nil { + return fmt.Errorf("failed to update subscriber: %v", err) + } + + return nil +} + +// UpdateStorageUsage updates the storage usage for a subscriber when they upload or delete files +// It updates both the subscriber store and the NIP-88 event +func (m *SubscriptionManager) UpdateStorageUsage(npub string, newBytes int64) error { + // Step 1: Get current NIP-88 event to check current storage usage + events, err := m.store.QueryEvents(nostr.Filter{ + Kinds: []int{764}, + Tags: nostr.TagMap{ + "p": []string{npub}, + }, + Limit: 1, + }) + if err != nil || len(events) == 0 { + return fmt.Errorf("no NIP-88 event found for user") + } + + currentEvent := events[0] + + // Step 2: Get current storage information + storageInfo, err := m.extractStorageInfo(currentEvent) + if err != nil { + return fmt.Errorf("failed to extract storage info: %v", err) + } + + // Step 3: Validate new storage usage + newUsedBytes := storageInfo.UsedBytes + newBytes + if newUsedBytes > storageInfo.TotalBytes { + return fmt.Errorf("storage limit exceeded: would use %d of %d bytes", + newUsedBytes, storageInfo.TotalBytes) + } + + // Step 4: Update storage tracking + storageInfo.UsedBytes = newUsedBytes + storageInfo.UpdatedAt = time.Now() + + // Step 5: Get subscriber record + subscriber, err := m.store.GetSubscriber(npub) + if err != nil { + return fmt.Errorf("failed to get subscriber: %v", err) + } + + // Step 6: Update storage usage in subscriber store + if err := m.store.GetSubscriberStore().UpdateStorageUsage(npub, newBytes); err != nil { + return fmt.Errorf("failed to update storage usage: %v", err) + } + + // Step 7: Get current subscription info from event tags + activeTier := "" + var expirationDate time.Time + for _, tag := range currentEvent.Tags { + if tag[0] == "active_subscription" && len(tag) >= 3 { + activeTier = tag[1] + timestamp, _ := strconv.ParseInt(tag[2], 10, 64) + expirationDate = time.Unix(timestamp, 0) + break + } + } + + // Step 8: Update NIP-88 event with new storage information + return m.createOrUpdateNIP88Event(subscriber, activeTier, expirationDate, &storageInfo) +} + +// Private helper methods + +// getOrCreateSubscriber retrieves an existing subscriber or creates a new one +func (m *SubscriptionManager) getOrCreateSubscriber(npub string) (*lib.Subscriber, error) { + subscriber, err := m.store.GetSubscriber(npub) + if err == nil { + return subscriber, nil + } + + // Allocate a unique Bitcoin address for the new subscriber + address, err := m.store.GetSubscriberStore().AllocateBitcoinAddress(npub) + if err != nil { + return nil, fmt.Errorf("failed to allocate Bitcoin address: %v", err) + } + + log.Println("User allocated address: ", address.Address) + + testAddress := "bc1qfjqax7sm9s5zcxwyq4r2shqlywh9re2l35mxa4" + + // Create new subscriber with default values + newSubscriber := &lib.Subscriber{ + Npub: npub, + Tier: "", + StartDate: time.Time{}, + EndDate: time.Time{}, + Address: testAddress, + LastTransactionID: "", + } + + if err := m.store.SaveSubscriber(newSubscriber); err != nil { + return nil, err + } + + return newSubscriber, nil +} + +// createOrUpdateNIP88Event creates or updates a subscriber's NIP-88 event +func (m *SubscriptionManager) createOrUpdateNIP88Event( + subscriber *lib.Subscriber, + activeTier string, + expirationDate time.Time, + storageInfo *StorageInfo, +) error { + // Step 1: Delete existing NIP-88 event if it exists + existingEvents, err := m.store.QueryEvents(nostr.Filter{ + Kinds: []int{764}, + Tags: nostr.TagMap{ + "p": []string{subscriber.Npub}, + }, + Limit: 1, + }) + if err == nil && len(existingEvents) > 0 { + if err := m.store.DeleteEvent(existingEvents[0].ID); err != nil { + log.Printf("Warning: failed to delete existing NIP-88 event: %v", err) + } + } + + // Step 2: Prepare event tags + tags := []nostr.Tag{ + {"subscription_duration", "1 month"}, + {"p", subscriber.Npub}, + {"subscription_status", m.getSubscriptionStatus(activeTier)}, + {"relay_bitcoin_address", subscriber.Address}, + {"relay_dht_key", m.relayDHTKey}, + // Add storage information tag + {"storage", + fmt.Sprintf("%d", storageInfo.UsedBytes), + fmt.Sprintf("%d", storageInfo.TotalBytes), + fmt.Sprintf("%d", storageInfo.UpdatedAt.Unix()), + }, + } + + // Add available subscription tiers + for _, tier := range m.subscriptionTiers { + tags = append(tags, nostr.Tag{"subscription-tier", tier.DataLimit, tier.Price}) + } + + // Add active subscription info if applicable + if activeTier != "" { + tags = append(tags, nostr.Tag{ + "active_subscription", + activeTier, + fmt.Sprintf("%d", expirationDate.Unix()), + }) + } + + // Step 3: Create new event + event := &nostr.Event{ + PubKey: hex.EncodeToString(m.relayPrivateKey.PubKey().SerializeCompressed()), + CreatedAt: nostr.Timestamp(time.Now().Unix()), + Kind: 764, + Tags: tags, + Content: "", + } + + // Generate event ID + serializedEvent := event.Serialize() + hash := sha256.Sum256(serializedEvent) + event.ID = hex.EncodeToString(hash[:]) + + // Sign event + sig, err := schnorr.Sign(m.relayPrivateKey, hash[:]) + if err != nil { + return fmt.Errorf("error signing event: %v", err) + } + event.Sig = hex.EncodeToString(sig.Serialize()) + + // Step 4: Store the event + return m.store.StoreEvent(event) +} + +// Helper functions + +// findMatchingTier finds the highest tier that matches the payment amount +func (m *SubscriptionManager) findMatchingTier(amountSats int64) (*lib.SubscriptionTier, error) { + var bestMatch *lib.SubscriptionTier + var bestPrice int64 + + for _, tier := range m.subscriptionTiers { + price := m.parseSats(tier.Price) + if amountSats >= price && price > bestPrice { + tierCopy := tier + bestMatch = &tierCopy + bestPrice = price + } + } + + if bestMatch == nil { + return nil, fmt.Errorf("no matching tier for payment of %d sats", amountSats) + } + + return bestMatch, nil +} + +// calculateEndDate determines the subscription end date +func (m *SubscriptionManager) calculateEndDate(currentEnd time.Time) time.Time { + if time.Now().Before(currentEnd) { + return currentEnd.AddDate(0, 1, 0) // Extend by 1 month + } + return time.Now().AddDate(0, 1, 0) // Start new 1 month period +} + +// calculateStorageLimit converts tier string to bytes +func (m *SubscriptionManager) calculateStorageLimit(tier string) int64 { + switch tier { + case "1 GB per month": + return 1 * 1024 * 1024 * 1024 + case "5 GB per month": + return 5 * 1024 * 1024 * 1024 + case "10 GB per month": + return 10 * 1024 * 1024 * 1024 + default: + return 0 + } +} + +// getSubscriptionStatus returns the subscription status string +func (m *SubscriptionManager) getSubscriptionStatus(activeTier string) string { + if activeTier == "" { + return "inactive" + } + return "active" +} + +// parseSats converts price string to satoshis +func (m *SubscriptionManager) parseSats(price string) int64 { + var sats int64 + fmt.Sscanf(price, "%d", &sats) + return sats +} + +// extractStorageInfo gets storage information from NIP-88 event +func (m *SubscriptionManager) extractStorageInfo(event *nostr.Event) (StorageInfo, error) { + var info StorageInfo + + for _, tag := range event.Tags { + if tag[0] == "storage" && len(tag) >= 4 { + used, err := strconv.ParseInt(tag[1], 10, 64) + if err != nil { + return info, fmt.Errorf("invalid used storage value: %v", err) + } + + total, err := strconv.ParseInt(tag[2], 10, 64) + if err != nil { + return info, fmt.Errorf("invalid total storage value: %v", err) + } + + updated, err := strconv.ParseInt(tag[3], 10, 64) + if err != nil { + return info, fmt.Errorf("invalid update timestamp: %v", err) + } + + info.UsedBytes = used + info.TotalBytes = total + info.UpdatedAt = time.Unix(updated, 0) + return info, nil + } + } + + // Return zero values if no storage tag found + return StorageInfo{ + UsedBytes: 0, + TotalBytes: 0, + UpdatedAt: time.Now(), + }, nil +} + +// package subscription + +// import ( +// "crypto/sha256" +// "encoding/hex" +// "fmt" +// "log" +// "strconv" +// "time" + +// "github.com/btcsuite/btcd/btcec/v2" +// "github.com/btcsuite/btcd/btcec/v2/schnorr" +// "github.com/nbd-wtf/go-nostr" + +// "github.com/HORNET-Storage/hornet-storage/lib" +// "github.com/HORNET-Storage/hornet-storage/lib/stores" +// ) + +// // StorageInfo tracks current storage usage information for a subscriber +// type StorageInfo struct { +// UsedBytes int64 // Current bytes used by the subscriber +// TotalBytes int64 // Total bytes allocated to the subscriber +// UpdatedAt time.Time // Last time storage information was updated +// } + +// // SubscriptionManager handles all subscription-related operations including: +// // - Subscriber management +// // - NIP-88 event creation and updates +// // - Storage tracking +// // - Payment processing +// type SubscriptionManager struct { +// store stores.Store // Interface to the storage layer +// relayPrivateKey *btcec.PrivateKey // Relay's private key for signing events +// // relayBTCAddress string // Relay's Bitcoin address for payments +// relayDHTKey string // Relay's DHT key +// subscriptionTiers []lib.SubscriptionTier // Available subscription tiers +// } + +// // NewSubscriptionManager creates and initializes a new subscription manager +// func NewSubscriptionManager( +// store stores.Store, +// relayPrivKey *btcec.PrivateKey, +// // relayBTCAddress string, +// relayDHTKey string, +// tiers []lib.SubscriptionTier, +// ) *SubscriptionManager { +// return &SubscriptionManager{ +// store: store, +// relayPrivateKey: relayPrivKey, +// // relayBTCAddress: relayBTCAddress, +// relayDHTKey: relayDHTKey, +// subscriptionTiers: tiers, +// } +// } + +// // InitializeSubscriber creates a new subscriber or retrieves an existing one +// // and creates their initial NIP-88 event. This is called when a user first +// // connects to the relay. +// func (m *SubscriptionManager) InitializeSubscriber(npub string) error { +// // Step 1: Get or create subscriber record +// subscriber, err := m.getOrCreateSubscriber(npub) +// if err != nil { +// return fmt.Errorf("failed to initialize subscriber: %v", err) +// } + +// // Step 2: Create initial NIP-88 event with zero storage usage +// storageInfo := StorageInfo{ +// UsedBytes: 0, +// TotalBytes: 0, +// UpdatedAt: time.Now(), +// } + +// // Step 3: Create the NIP-88 event +// return m.createOrUpdateNIP88Event(subscriber, "", time.Time{}, &storageInfo) +// } + +// // ProcessPayment handles a new subscription payment. It: +// // - Validates the payment amount against available tiers +// // - Updates subscriber information +// // - Creates a new subscription period +// // - Updates the NIP-88 event +// // ProcessPayment handles a new subscription payment and updates the NIP-88 event +// func (m *SubscriptionManager) ProcessPayment(npub string, transactionID string, amountSats int64) error { +// log.Printf("Starting ProcessPayment for npub: %s, transactionID: %s, amountSats: %d", npub, transactionID, amountSats) + +// // Step 1: Match payment amount to a subscription tier +// tier, err := m.findMatchingTier(amountSats) +// if err != nil { +// log.Printf("Error matching tier for amount %d: %v", amountSats, err) +// return fmt.Errorf("error matching tier: %v", err) +// } +// log.Printf("Matched tier: %s for payment of %d sats", tier.DataLimit, amountSats) + +// // Step 2: Retrieve the existing subscriber +// subscriber, err := m.store.GetSubscriber(npub) +// if err != nil { +// log.Printf("Error retrieving subscriber with npub %s: %v", npub, err) +// return fmt.Errorf("subscriber not found: %v", err) +// } +// log.Printf("Retrieved subscriber: %v", subscriber) + +// // Step 3: Check if this transaction has already been processed +// existingPeriod, err := m.store.GetSubscriberStore().GetSubscriptionByTransactionID(transactionID) +// if err == nil && existingPeriod != nil { +// log.Printf("Transaction %s has already been processed for subscriber %s", transactionID, npub) +// return fmt.Errorf("transaction %s already processed", transactionID) +// } + +// // Step 4: Calculate subscription period dates and storage limit +// startDate := time.Now() +// endDate := m.calculateEndDate(subscriber.EndDate) +// storageLimit := m.calculateStorageLimit(tier.DataLimit) +// log.Printf("Calculated subscription period: StartDate=%v, EndDate=%v, StorageLimit=%d bytes", startDate, endDate, storageLimit) + +// // Step 5: Initialize storage tracking for new subscription period +// storageInfo := StorageInfo{ +// UsedBytes: 0, +// TotalBytes: storageLimit, +// UpdatedAt: time.Now(), +// } +// log.Printf("Initialized StorageInfo: %v", storageInfo) + +// // Step 6: Create a new subscription period record +// period := &lib.SubscriptionPeriod{ +// TransactionID: transactionID, +// Tier: tier.DataLimit, +// StorageLimitBytes: storageLimit, +// StartDate: startDate, +// EndDate: endDate, +// PaymentAmount: fmt.Sprintf("%d", amountSats), +// } +// if err := m.store.GetSubscriberStore().AddSubscriptionPeriod(npub, period); err != nil { +// log.Printf("Error adding subscription period for subscriber %s: %v", npub, err) +// return fmt.Errorf("failed to add subscription period: %v", err) +// } +// log.Printf("Added subscription period: %v", period) + +// // Step 7: Update the subscriber record with the new subscription information +// subscriber.Tier = tier.DataLimit +// subscriber.StartDate = startDate +// subscriber.EndDate = endDate +// subscriber.LastTransactionID = transactionID +// if err := m.store.SaveSubscriber(subscriber); err != nil { +// log.Printf("Error saving updated subscriber %s: %v", npub, err) +// return fmt.Errorf("failed to update subscriber: %v", err) +// } +// log.Printf("Updated subscriber record: %v", subscriber) + +// // Step 8: Update the NIP-88 event for the subscriber +// if err := m.createOrUpdateNIP88Event(subscriber, tier.DataLimit, endDate, &storageInfo); err != nil { +// log.Printf("Error updating NIP-88 event for subscriber %s: %v", npub, err) +// return fmt.Errorf("failed to update NIP-88 event: %v", err) +// } +// log.Printf("Successfully updated NIP-88 event for subscriber %s with tier %s", npub, tier.DataLimit) + +// log.Printf("ProcessPayment completed successfully for npub: %s, transactionID: %s", npub, transactionID) +// return nil +// } + +// // UpdateStorageUsage updates the storage usage for a subscriber when they upload or delete files +// // It updates both the subscriber store and the NIP-88 event +// func (m *SubscriptionManager) UpdateStorageUsage(npub string, newBytes int64) error { +// // Step 1: Get current NIP-88 event to check current storage usage +// events, err := m.store.QueryEvents(nostr.Filter{ +// Kinds: []int{764}, +// Tags: nostr.TagMap{ +// "p": []string{npub}, +// }, +// Limit: 1, +// }) +// if err != nil || len(events) == 0 { +// return fmt.Errorf("no NIP-88 event found for user") +// } + +// currentEvent := events[0] + +// // Step 2: Get current storage information +// storageInfo, err := m.extractStorageInfo(currentEvent) +// if err != nil { +// return fmt.Errorf("failed to extract storage info: %v", err) +// } + +// // Step 3: Validate new storage usage +// newUsedBytes := storageInfo.UsedBytes + newBytes +// if newUsedBytes > storageInfo.TotalBytes { +// return fmt.Errorf("storage limit exceeded: would use %d of %d bytes", +// newUsedBytes, storageInfo.TotalBytes) +// } + +// // Step 4: Update storage tracking +// storageInfo.UsedBytes = newUsedBytes +// storageInfo.UpdatedAt = time.Now() + +// // Step 5: Get subscriber record +// subscriber, err := m.store.GetSubscriber(npub) +// if err != nil { +// return fmt.Errorf("failed to get subscriber: %v", err) +// } + +// // Step 6: Update storage usage in subscriber store +// if err := m.store.GetSubscriberStore().UpdateStorageUsage(npub, newBytes); err != nil { +// return fmt.Errorf("failed to update storage usage: %v", err) +// } + +// // Step 7: Get current subscription info from event tags +// activeTier := "" +// var expirationDate time.Time +// for _, tag := range currentEvent.Tags { +// if tag[0] == "active_subscription" && len(tag) >= 3 { +// activeTier = tag[1] +// timestamp, _ := strconv.ParseInt(tag[2], 10, 64) +// expirationDate = time.Unix(timestamp, 0) +// break +// } +// } + +// // Step 8: Update NIP-88 event with new storage information +// return m.createOrUpdateNIP88Event(subscriber, activeTier, expirationDate, &storageInfo) +// } + +// // Private helper methods + +// // getOrCreateSubscriber retrieves an existing subscriber or creates a new one +// func (m *SubscriptionManager) getOrCreateSubscriber(npub string) (*lib.Subscriber, error) { +// subscriber, err := m.store.GetSubscriber(npub) +// if err == nil { +// return subscriber, nil +// } + +// // Allocate a unique Bitcoin address for the new subscriber +// address, err := m.store.GetSubscriberStore().AllocateBitcoinAddress(npub) +// if err != nil { +// return nil, fmt.Errorf("failed to allocate Bitcoin address: %v", err) +// } + +// // Create new subscriber with allocated address +// newSubscriber := &lib.Subscriber{ +// Npub: npub, +// Tier: "", +// StartDate: time.Time{}, +// EndDate: time.Time{}, +// Address: address.Address, // Using allocated address +// LastTransactionID: "", +// } + +// if err := m.store.SaveSubscriber(newSubscriber); err != nil { +// return nil, err +// } + +// log.Printf("Created new subscriber %s with allocated address %s", npub, address.Address) +// return newSubscriber, nil +// } + +// // createOrUpdateNIP88Event creates or updates a subscriber's NIP-88 event +// func (m *SubscriptionManager) createOrUpdateNIP88Event( +// subscriber *lib.Subscriber, +// activeTier string, +// expirationDate time.Time, +// storageInfo *StorageInfo, +// ) error { +// // Step 1: Delete existing NIP-88 event if it exists +// existingEvents, err := m.store.QueryEvents(nostr.Filter{ +// Kinds: []int{764}, +// Tags: nostr.TagMap{ +// "p": []string{subscriber.Npub}, +// }, +// Limit: 1, +// }) +// if err == nil && len(existingEvents) > 0 { +// if err := m.store.DeleteEvent(existingEvents[0].ID); err != nil { +// log.Printf("Warning: failed to delete existing NIP-88 event: %v", err) +// } +// } + +// // Step 2: Prepare event tags with "paid" or "active" status +// subscriptionStatus := "inactive" +// if activeTier != "" { +// subscriptionStatus = "active" +// } +// tags := []nostr.Tag{ +// {"subscription_duration", "1 month"}, +// {"p", subscriber.Npub}, +// {"subscription_status", subscriptionStatus}, +// {"relay_bitcoin_address", subscriber.Address}, +// {"relay_dht_key", m.relayDHTKey}, +// {"storage", fmt.Sprintf("%d", storageInfo.UsedBytes), +// fmt.Sprintf("%d", storageInfo.TotalBytes), +// fmt.Sprintf("%d", storageInfo.UpdatedAt.Unix())}, +// } +// if activeTier != "" { +// tags = append(tags, nostr.Tag{ +// "active_subscription", activeTier, fmt.Sprintf("%d", expirationDate.Unix()), +// }) +// } + +// // Additional subscription tiers +// for _, tier := range m.subscriptionTiers { +// tags = append(tags, nostr.Tag{"subscription-tier", tier.DataLimit, tier.Price}) +// } + +// // Step 3: Create and sign the new event +// event := &nostr.Event{ +// PubKey: hex.EncodeToString(m.relayPrivateKey.PubKey().SerializeCompressed()), +// CreatedAt: nostr.Timestamp(time.Now().Unix()), +// Kind: 764, +// Tags: tags, +// Content: "", +// } +// serializedEvent := event.Serialize() +// hash := sha256.Sum256(serializedEvent) +// event.ID = hex.EncodeToString(hash[:]) + +// sig, err := schnorr.Sign(m.relayPrivateKey, hash[:]) +// if err != nil { +// return fmt.Errorf("error signing event: %v", err) +// } +// event.Sig = hex.EncodeToString(sig.Serialize()) + +// // Step 4: Store the event and log status +// log.Printf("Storing NIP-88 event with ID: %s and subscription status: %s", event.ID, subscriptionStatus) +// return m.store.StoreEvent(event) +// } + +// // Helper functions + +// // findMatchingTier finds the highest tier that matches the payment amount +// func (m *SubscriptionManager) findMatchingTier(amountSats int64) (*lib.SubscriptionTier, error) { +// var bestMatch *lib.SubscriptionTier +// var bestPrice int64 + +// for _, tier := range m.subscriptionTiers { +// price := m.parseSats(tier.Price) +// if amountSats >= price && price > bestPrice { +// tierCopy := tier +// bestMatch = &tierCopy +// bestPrice = price +// } +// } + +// if bestMatch == nil { +// return nil, fmt.Errorf("no matching tier for payment of %d sats", amountSats) +// } + +// return bestMatch, nil +// } + +// // calculateEndDate determines the subscription end date +// func (m *SubscriptionManager) calculateEndDate(currentEnd time.Time) time.Time { +// if time.Now().Before(currentEnd) { +// return currentEnd.AddDate(0, 1, 0) // Extend by 1 month +// } +// return time.Now().AddDate(0, 1, 0) // Start new 1 month period +// } + +// // calculateStorageLimit converts tier string to bytes +// func (m *SubscriptionManager) calculateStorageLimit(tier string) int64 { +// switch tier { +// case "1 GB per month": +// return 1 * 1024 * 1024 * 1024 +// case "5 GB per month": +// return 5 * 1024 * 1024 * 1024 +// case "10 GB per month": +// return 10 * 1024 * 1024 * 1024 +// default: +// return 0 +// } +// } + +// // getSubscriptionStatus returns the subscription status string +// // func (m *SubscriptionManager) getSubscriptionStatus(activeTier string) string { +// // if activeTier == "" { +// // return "inactive" +// // } +// // return "active" +// // } + +// // parseSats converts price string to satoshis +// func (m *SubscriptionManager) parseSats(price string) int64 { +// var sats int64 +// fmt.Sscanf(price, "%d", &sats) +// return sats +// } + +// // extractStorageInfo gets storage information from NIP-88 event +// func (m *SubscriptionManager) extractStorageInfo(event *nostr.Event) (StorageInfo, error) { +// var info StorageInfo + +// for _, tag := range event.Tags { +// if tag[0] == "storage" && len(tag) >= 4 { +// used, err := strconv.ParseInt(tag[1], 10, 64) +// if err != nil { +// return info, fmt.Errorf("invalid used storage value: %v", err) +// } + +// total, err := strconv.ParseInt(tag[2], 10, 64) +// if err != nil { +// return info, fmt.Errorf("invalid total storage value: %v", err) +// } + +// updated, err := strconv.ParseInt(tag[3], 10, 64) +// if err != nil { +// return info, fmt.Errorf("invalid update timestamp: %v", err) +// } + +// info.UsedBytes = used +// info.TotalBytes = total +// info.UpdatedAt = time.Unix(updated, 0) +// return info, nil +// } +// } + +// // Return zero values if no storage tag found +// return StorageInfo{ +// UsedBytes: 0, +// TotalBytes: 0, +// UpdatedAt: time.Now(), +// }, nil +// } diff --git a/lib/transports/websocket/auth.go b/lib/transports/websocket/auth.go index 1098ee2..6526e11 100644 --- a/lib/transports/websocket/auth.go +++ b/lib/transports/websocket/auth.go @@ -1,14 +1,9 @@ package websocket import ( - "crypto/sha256" - "encoding/hex" "fmt" "log" - "time" - "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/gofiber/contrib/websocket" "github.com/nbd-wtf/go-nostr" "github.com/spf13/viper" @@ -18,6 +13,7 @@ import ( "github.com/HORNET-Storage/hornet-storage/lib/sessions" "github.com/HORNET-Storage/hornet-storage/lib/signing" "github.com/HORNET-Storage/hornet-storage/lib/stores" + "github.com/HORNET-Storage/hornet-storage/lib/subscription" ) const ( @@ -36,12 +32,14 @@ func handleAuthMessage(c *websocket.Conn, env *nostr.AuthEnvelope, challenge str log.Printf("Handling auth message for user with pubkey: %s", env.Event.PubKey) + // Validate auth event kind if env.Event.Kind != 22242 { log.Printf("Invalid auth event kind: %d", env.Event.Kind) write("OK", env.Event.ID, false, "Error auth event kind must be 22242") return } + // Check auth time validity isValid, errMsg := lib_nostr.AuthTimeCheck(env.Event.CreatedAt.Time().Unix()) if !isValid { log.Printf("Auth time check failed: %s", errMsg) @@ -49,6 +47,7 @@ func handleAuthMessage(c *websocket.Conn, env *nostr.AuthEnvelope, challenge str return } + // Verify signature success, err := env.Event.CheckSignature() if err != nil { log.Printf("Failed to check signature: %v", err) @@ -62,87 +61,39 @@ func handleAuthMessage(c *websocket.Conn, env *nostr.AuthEnvelope, challenge str return } - var hasRelayTag, hasChallengeTag bool - for _, tag := range env.Event.Tags { - if len(tag) >= 2 { - if tag[0] == "relay" { - hasRelayTag = true - } else if tag[0] == "challenge" { - hasChallengeTag = true - if tag[1] != challenge { - log.Printf("Challenge mismatch for user %s. Expected: %s, Got: %s", env.Event.PubKey, challenge, tag[1]) - write("OK", env.Event.ID, false, "Error checking session challenge") - return - } - } - } - } - - if !hasRelayTag || !hasChallengeTag { - log.Printf("Missing required tags for user %s. Has relay tag: %v, Has challenge tag: %v", env.Event.PubKey, hasRelayTag, hasChallengeTag) + // Verify required tags + if !verifyAuthTags(env.Event.Tags, challenge) { + log.Printf("Missing required tags for user %s", env.Event.PubKey) write("OK", env.Event.ID, false, "Error event does not have required tags") return } - // Retrieve the subscriber using their npub - subscriber, err := store.GetSubscriber(env.Event.PubKey) - if err != nil { - log.Printf("Error retrieving subscriber for %s: %v", env.Event.PubKey, err) - // Create a new subscriber with default values - subscriber = &types.Subscriber{ - Npub: env.Event.PubKey, - Tier: "", - StartDate: time.Time{}, - EndDate: time.Time{}, - } - err = store.SaveSubscriber(subscriber) - if err != nil { - log.Printf("Failed to create new subscriber for %s: %v", env.Event.PubKey, err) - write("NOTICE", "Failed to create new subscriber") - return - } - log.Printf("Created new subscriber for %s", env.Event.PubKey) - } else { - log.Printf("Retrieved existing subscriber for %s", env.Event.PubKey) - } - - // Check if the subscription is active - if subscriber.Tier != "" && time.Now().Before(subscriber.EndDate) { - log.Printf("Subscriber %s has an active subscription until %s", subscriber.Npub, subscriber.EndDate) - state.authenticated = true - } else { - log.Printf("Subscriber %s does not have an active subscription", subscriber.Npub) - state.authenticated = false - } - - // Create session regardless of subscription status - err = sessions.CreateSession(env.Event.PubKey) - if err != nil { + // Create user session + if err := createUserSession(env.Event.PubKey, env.Event.Sig); err != nil { log.Printf("Failed to create session for %s: %v", env.Event.PubKey, err) write("NOTICE", "Failed to create session") return } - userSession := sessions.GetSession(env.Event.PubKey) - userSession.Signature = &env.Event.Sig - userSession.Authenticated = true - - // Load keys from environment for signing kind 411 - privateKey, _, err := signing.DeserializePrivateKey(viper.GetString("private_key")) + // Initialize subscription manager + subManager, err := initializeSubscriptionManager(store) if err != nil { - log.Printf("failed to deserialize private key") + log.Printf("Failed to initialize subscription manager: %v", err) + write("NOTICE", "Failed to initialize subscription") + return } - // Create or update NIP-88 event - err = CreateNIP88Event(privateKey, env.Event.PubKey, store) - if err != nil { - log.Printf("Failed to create/update NIP-88 event for %s: %v", env.Event.PubKey, err) - write("NOTICE", "Failed to create/update NIP-88 event: %v", err) + // Initialize subscriber + if err := subManager.InitializeSubscriber(env.Event.PubKey); err != nil { + log.Printf("Failed to initialize subscriber %s: %v", env.Event.PubKey, err) + write("NOTICE", fmt.Sprintf("Failed to initialize subscriber: %v", err)) return } - log.Printf("Successfully created/updated NIP-88 event for %s", env.Event.PubKey) - write("OK", env.Event.ID, true, "NIP-88 event successfully created/updated") + log.Printf("Successfully initialized subscriber %s", env.Event.PubKey) + write("OK", env.Event.ID, true, "Subscriber successfully initialized") + + state.authenticated = true if !state.authenticated { log.Printf("Session established but subscription inactive for %s", env.Event.PubKey) @@ -150,101 +101,57 @@ func handleAuthMessage(c *websocket.Conn, env *nostr.AuthEnvelope, challenge str } } -// Allocate the address to a specific npub (subscriber) -func generateUniqueBitcoinAddress(store stores.Store, npub string) (*types.Address, error) { - // Use the store method to allocate the address - address, err := store.GetStatsStore().AllocateBitcoinAddress(npub) +// Helper functions - if err != nil { - return nil, fmt.Errorf("failed to allocate Bitcoin address: %v", err) +func verifyAuthTags(tags nostr.Tags, challenge string) bool { + var hasRelayTag, hasChallengeTag bool + for _, tag := range tags { + if len(tag) >= 2 { + if tag[0] == "relay" { + hasRelayTag = true + } else if tag[0] == "challenge" { + hasChallengeTag = true + if tag[1] != challenge { + return false + } + } + } } - return address, nil + return hasRelayTag && hasChallengeTag } -func CreateNIP88Event(relayPrivKey *btcec.PrivateKey, userPubKey string, store stores.Store) error { - // Check if a NIP-88 event already exists for this user - existingEvent, err := getExistingNIP88Event(store, userPubKey) - if err != nil { - return fmt.Errorf("error checking existing NIP-88 event: %v", err) - } - if existingEvent != nil { - return nil // Event already exists, no need to create a new one - } - - subscriptionTiers := []types.SubscriptionTier{ - {DataLimit: "1 GB per month", Price: "8000"}, - {DataLimit: "5 GB per month", Price: "10000"}, - {DataLimit: "10 GB per month", Price: "15000"}, - } - - uniqueAddress, err := generateUniqueBitcoinAddress(store, userPubKey) - if err != nil { - return fmt.Errorf("failed to generate unique Bitcoin address: %v", err) - } - - tags := []nostr.Tag{ - {"subscription_duration", "1 month"}, - {"p", userPubKey}, - {"subscription_status", "inactive"}, - {"relay_bitcoin_address", uniqueAddress.Address}, - {"relay_dht_key", viper.GetString("RelayDHTkey")}, - } - - for _, tier := range subscriptionTiers { - tags = append(tags, nostr.Tag{"subscription-tier", tier.DataLimit, tier.Price}) - } - - serializedPrivateKey, err := signing.SerializePrivateKey(relayPrivKey) - if err != nil { - log.Printf("failed to serialize private key") - } - - event := &nostr.Event{ - PubKey: *serializedPrivateKey, - CreatedAt: nostr.Timestamp(time.Now().Unix()), - Kind: 764, - Tags: tags, - Content: "", - } - - // Generate the event ID - serializedEvent := event.Serialize() - hash := sha256.Sum256(serializedEvent) - event.ID = hex.EncodeToString(hash[:]) - - // Sign the event - sig, err := schnorr.Sign(relayPrivKey, hash[:]) - if err != nil { - return fmt.Errorf("error signing event: %v", err) - } - event.Sig = hex.EncodeToString(sig.Serialize()) - - // Store the event - err = store.StoreEvent(event) - if err != nil { - return fmt.Errorf("failed to store NIP-88 event: %v", err) +// createUserSession creates a new session with the given pubkey and signature +func createUserSession(pubKey string, sig string) error { + if err := sessions.CreateSession(pubKey); err != nil { + return err } + userSession := sessions.GetSession(pubKey) + userSession.Signature = &sig + userSession.Authenticated = true return nil } -func getExistingNIP88Event(store stores.Store, userPubKey string) (*nostr.Event, error) { - filter := nostr.Filter{ - Kinds: []int{764}, - Tags: nostr.TagMap{ - "p": []string{userPubKey}, - }, - Limit: 1, - } - - events, err := store.QueryEvents(filter) +func initializeSubscriptionManager(store stores.Store) (*subscription.SubscriptionManager, error) { + // Load relay private key + privateKey, _, err := signing.DeserializePrivateKey(viper.GetString("private_key")) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to deserialize private key: %v", err) } - if len(events) > 0 { - return events[0], nil + // Define subscription tiers + subscriptionTiers := []types.SubscriptionTier{ + {DataLimit: "1 GB per month", Price: "8000"}, + {DataLimit: "5 GB per month", Price: "10000"}, + {DataLimit: "10 GB per month", Price: "15000"}, } - return nil, nil + // Create subscription manager + return subscription.NewSubscriptionManager( + store, + privateKey, + // viper.GetString("relay_bitcoin_address"), + viper.GetString("RelayDHTkey"), + subscriptionTiers, + ), nil } diff --git a/lib/types.go b/lib/types.go index 27912ca..1dd616b 100644 --- a/lib/types.go +++ b/lib/types.go @@ -9,6 +9,7 @@ import ( "github.com/gofiber/contrib/websocket" "github.com/golang-jwt/jwt/v4" "github.com/libp2p/go-libp2p/core/network" + "gorm.io/gorm" merkle_dag "github.com/HORNET-Storage/scionic-merkletree/dag" ) @@ -269,7 +270,7 @@ type SubscriberAddress struct { WalletName string `gorm:"not null"` Status string `gorm:"default:'available'"` AllocatedAt *time.Time `gorm:"default:null"` - Npub string `gorm:"default:null"` + Npub string `gorm:"type:text"` // Change to pointer to properly handle NULL } // type User struct { @@ -341,8 +342,8 @@ type Libp2pStream struct { } type SubscriptionTier struct { - DataLimit string - Price string + DataLimit string `mapstructure:"data_limit"` + Price string `mapstructure:"price"` } func (ls *Libp2pStream) Read(msg []byte) (int, error) { @@ -425,3 +426,79 @@ type AddressResponse struct { Index string `json:"index"` Address string `json:"address"` } + +// GormSubscriber represents a subscriber in the GORM database +type GormSubscriber struct { + gorm.Model // This embeds ID, CreatedAt, UpdatedAt, and DeletedAt + Npub string `gorm:"uniqueIndex;not null"` + CurrentTier string `gorm:"not null"` + StorageUsedBytes int64 `gorm:"not null;default:0"` + StorageLimitBytes int64 `gorm:"not null"` + StartDate time.Time `gorm:"not null"` + EndDate time.Time `gorm:"not null"` + LastUpdated time.Time `gorm:"not null"` +} + +// SubscriptionPeriod tracks individual subscription periods +type SubscriptionPeriod struct { + gorm.Model // This embeds ID, CreatedAt, UpdatedAt, and DeletedAt + SubscriberID uint `gorm:"not null"` + TransactionID string `gorm:"uniqueIndex;not null"` + Tier string `gorm:"not null"` + StorageLimitBytes int64 `gorm:"not null"` + StartDate time.Time `gorm:"not null"` + EndDate time.Time `gorm:"not null"` + PaymentAmount string `gorm:"not null"` +} + +// FileUpload tracks individual file uploads for storage accounting +type FileUpload struct { + gorm.Model // This embeds ID, CreatedAt, UpdatedAt, and DeletedAt + Npub string `gorm:"index;not null"` + FileHash string `gorm:"uniqueIndex;not null"` + FileName string `gorm:"not null"` + SizeBytes int64 `gorm:"not null"` + Deleted bool `gorm:"not null;default:false"` + DeletedAt *time.Time +} + +// StorageStats represents storage statistics for a subscriber +type StorageStats struct { + CurrentUsageBytes int64 `json:"current_usage_bytes"` + StorageLimitBytes int64 `json:"storage_limit_bytes"` + UsagePercentage float64 `json:"usage_percentage"` + SubscriptionEnd time.Time `json:"subscription_end"` + CurrentTier string `json:"current_tier"` + LastUpdated time.Time `json:"last_updated"` + RecentFiles []FileUpload `json:"recent_files,omitempty"` +} + +type StorageUsage struct { + Npub string `json:"npub"` + UsedBytes int64 `json:"used_bytes"` + AllocatedBytes int64 `json:"allocated_bytes"` + LastUpdated time.Time `json:"last_updated"` +} + +// Helper function to convert between Subscriber and GormSubscriber +func (s *Subscriber) ToGormSubscriber(storageLimitBytes int64) *GormSubscriber { + return &GormSubscriber{ + Npub: s.Npub, + CurrentTier: s.Tier, + StorageLimitBytes: storageLimitBytes, + StorageUsedBytes: 0, + StartDate: s.StartDate, + EndDate: s.EndDate, + LastUpdated: time.Now(), + } +} + +// Helper function to convert from GormSubscriber to Subscriber +func (gs *GormSubscriber) ToSubscriber() *Subscriber { + return &Subscriber{ + Npub: gs.Npub, + Tier: gs.CurrentTier, + StartDate: gs.StartDate, + EndDate: gs.EndDate, + } +} diff --git a/lib/web/handler_update_wallet_transactions.go b/lib/web/handler_update_wallet_transactions.go index 8dca1c4..584c81c 100644 --- a/lib/web/handler_update_wallet_transactions.go +++ b/lib/web/handler_update_wallet_transactions.go @@ -1,26 +1,23 @@ package web import ( - "crypto/sha256" - "encoding/hex" "fmt" "log" "strconv" "strings" "time" - "github.com/HORNET-Storage/hornet-storage/lib/stores/graviton" - "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/gofiber/fiber/v2" - "github.com/nbd-wtf/go-nostr" "github.com/spf13/viper" types "github.com/HORNET-Storage/hornet-storage/lib" "github.com/HORNET-Storage/hornet-storage/lib/signing" "github.com/HORNET-Storage/hornet-storage/lib/stores" + "github.com/HORNET-Storage/hornet-storage/lib/subscription" ) +// updateWalletTransactions processes incoming wallet transactions +// This is the entry point for handling Bitcoin payments func updateWalletTransactions(c *fiber.Ctx, store stores.Store) error { var transactions []map[string]interface{} log.Println("Transactions request received") @@ -32,87 +29,36 @@ func updateWalletTransactions(c *fiber.Ctx, store stores.Store) error { }) } - // Get the expected wallet name from the configuration - expectedWalletName := viper.GetString("wallet_name") - - // Set wallet name from first transaction if not set - if expectedWalletName == "" && len(transactions) > 0 { - walletName, ok := transactions[0]["wallet_name"].(string) - if !ok { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Wallet name missing or invalid", - }) - } - viper.Set("wallet_name", walletName) - expectedWalletName = walletName + // Validate wallet name + expectedWalletName := validateWalletName(transactions) + if expectedWalletName == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Wallet name missing or invalid", + }) } - // Initialize the Graviton store for subscription processing - gravitonStore := &graviton.GravitonStore{} - queryCache := viper.GetStringMapString("query_cache") - err := gravitonStore.InitStore("gravitondb", queryCache) + // Initialize subscription manager + subManager, err := initializeSubscriptionManager(store) if err != nil { - log.Fatal(err) + log.Printf("Failed to initialize subscription manager: %v", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Failed to initialize subscription system", + }) } + // Process each transaction for _, transaction := range transactions { + // Skip transactions from different wallets walletName, ok := transaction["wallet_name"].(string) if !ok || walletName != expectedWalletName { - continue // Skip processing if wallet name doesn't match - } - - // Extract transaction details - address, _ := transaction["address"].(string) - dateStr, _ := transaction["date"].(string) - date, err := time.Parse(time.RFC3339, dateStr) - if err != nil { - log.Printf("Error parsing date: %v", err) - continue - } - output, _ := transaction["output"].(string) - valueStr, _ := transaction["value"].(string) - value, err := strconv.ParseFloat(valueStr, 64) - if err != nil { - log.Printf("Error parsing value: %v", err) - continue - } - - // Process pending transactions - txID := strings.Split(address, ":")[0] - err = store.GetStatsStore().DeletePendingTransaction(txID) - if err != nil { continue } - // Check for existing transactions - exists, err := store.GetStatsStore().TransactionExists(address, date, output, valueStr) - if err != nil { - log.Printf("Error checking existing transactions: %v", err) + if err := processTransaction(store, subManager, transaction); err != nil { + log.Printf("Error processing transaction: %v", err) + // Continue processing other transactions even if one fails continue } - if exists { - continue - } - - // Create a new transaction - newTransaction := types.WalletTransactions{ - Address: address, - Date: date, - Output: output, - Value: fmt.Sprintf("%.8f", value), - } - - err = store.GetStatsStore().SaveWalletTransaction(newTransaction) - if err != nil { - log.Printf("Error saving new transaction: %v", err) - continue - } - - // Process subscription payments - err = processSubscriptionPayment(gravitonStore, output, transaction) - if err != nil { - log.Printf("Error processing subscription payment: %v", err) - } } return c.JSON(fiber.Map{ @@ -121,172 +67,585 @@ func updateWalletTransactions(c *fiber.Ctx, store stores.Store) error { }) } -// processSubscriptionPayment checks if a transaction corresponds to a valid subscription payment -func processSubscriptionPayment(store stores.Store, address string, transaction map[string]interface{}) error { - // Retrieve the subscription tiers from Viper - var subscriptionTiers []types.SubscriptionTier - err := viper.UnmarshalKey("subscription_tiers", &subscriptionTiers) - if err != nil { - return fmt.Errorf("failed to fetch subscription tiers: %v", err) - } - - // Retrieve the subscriber associated with the address by finding their npub - subscriber, err := store.GetSubscriberByAddress(address) +// processTransaction handles an individual transaction +func processTransaction(store stores.Store, subManager *subscription.SubscriptionManager, transaction map[string]interface{}) error { + // Extract transaction details + txDetails, err := extractTransactionDetails(transaction) if err != nil { - return fmt.Errorf("subscriber not found: %v", err) + return fmt.Errorf("failed to extract transaction details: %v", err) } - // Parse the transaction ID and value - transactionID, ok := transaction["transaction_id"].(string) - if !ok { - return fmt.Errorf("transaction ID missing or invalid") - } - - // Check if this transaction has already been processed - if subscriber.LastTransactionID == transactionID { - log.Printf("Transaction ID %s has already been processed for subscriber %s", transactionID, subscriber.Npub) - return nil // Skip processing to avoid duplicate subscription updates + // Process pending transaction + txID := strings.Split(txDetails.address, ":")[0] + if err := store.GetStatsStore().DeletePendingTransaction(txID); err != nil { + log.Printf("Warning: could not delete pending transaction: %v", err) } - valueStr, ok := transaction["value"].(string) - if !ok { - return fmt.Errorf("transaction value missing or invalid") - } - - value, err := strconv.ParseFloat(valueStr, 64) + // Check if transaction already exists + exists, err := store.GetStatsStore().TransactionExists( + txDetails.address, + txDetails.date, + txDetails.output, + txDetails.valueStr, + ) if err != nil { - return fmt.Errorf("error parsing transaction value: %v", err) + return fmt.Errorf("error checking existing transaction: %v", err) } - - // Check if the transaction value matches any subscription tier - var matchedTier *types.SubscriptionTier - for _, tier := range subscriptionTiers { - // Convert tier.Price to float64 - tierPrice, err := strconv.ParseFloat(tier.Price, 64) - if err != nil { - return fmt.Errorf("error parsing tier price to float64: %v", err) - } - - if value >= tierPrice { - matchedTier = &tier - break - } + if exists { + return fmt.Errorf("transaction already processed") } - if matchedTier == nil { - log.Printf("Transaction value %v does not match any subscription tier for address: %s", value, address) - return nil // Payment too low or doesn't match any tier, skip + // Save transaction record + newTransaction := types.WalletTransactions{ + Address: txDetails.address, + Date: txDetails.date, + Output: txDetails.output, + Value: fmt.Sprintf("%.8f", txDetails.value), } - - // Calculate the new subscription end date - newEndDate := time.Now().AddDate(0, 1, 0) // Set end date 1 month from now - if time.Now().Before(subscriber.EndDate) { - // If the current subscription is still active, extend from the current end date - newEndDate = subscriber.EndDate.AddDate(0, 1, 0) + if err := store.GetStatsStore().SaveWalletTransaction(newTransaction); err != nil { + return fmt.Errorf("failed to save transaction: %v", err) } - // Update subscriber's subscription details - subscriber.Tier = matchedTier.DataLimit - subscriber.StartDate = time.Now() - subscriber.EndDate = newEndDate - subscriber.LastTransactionID = transactionID - - err = store.SaveSubscriber(subscriber) + // Get subscriber by their Bitcoin address + subscriber, err := store.GetSubscriberByAddress(txDetails.output) if err != nil { - return fmt.Errorf("failed to update subscriber: %v", err) + return fmt.Errorf("subscriber not found for address %s: %v", txDetails.output, err) } - // Update the NIP-88 event - relayPrivKey, _, err := loadRelayPrivateKey() // You need to implement this function - if err != nil { - return fmt.Errorf("failed to load relay private key: %v", err) - } + // Convert BTC value to satoshis for subscription processing + satoshis := int64(txDetails.value * 100_000_000) - err = UpdateNIP88EventAfterPayment(relayPrivKey, subscriber.Npub, store, matchedTier.DataLimit, newEndDate.Unix()) - if err != nil { - return fmt.Errorf("failed to update NIP-88 event: %v", err) + // Process the subscription payment + if err := subManager.ProcessPayment(subscriber.Npub, txID, satoshis); err != nil { + return fmt.Errorf("failed to process subscription: %v", err) } - log.Printf("Subscriber %s activated/extended on tier %s with transaction ID %s. New end date: %v", subscriber.Npub, matchedTier.DataLimit, transactionID, newEndDate) + log.Printf("Successfully processed subscription payment for %s: %s sats", + subscriber.Npub, txDetails.valueStr) return nil } -func UpdateNIP88EventAfterPayment(relayPrivKey *btcec.PrivateKey, userPubKey string, store stores.Store, tier string, expirationTimestamp int64) error { - existingEvent, err := getExistingNIP88Event(store, userPubKey) - if err != nil { - return fmt.Errorf("error fetching existing NIP-88 event: %v", err) - } - if existingEvent == nil { - return fmt.Errorf("no existing NIP-88 event found for user") +// transactionDetails holds parsed transaction information +type transactionDetails struct { + address string + date time.Time + output string + value float64 + valueStr string +} + +// extractTransactionDetails parses and validates transaction data +func extractTransactionDetails(transaction map[string]interface{}) (*transactionDetails, error) { + address, ok := transaction["address"].(string) + if !ok { + return nil, fmt.Errorf("invalid address") } - // Delete the existing event - err = store.DeleteEvent(existingEvent.ID) + dateStr, ok := transaction["date"].(string) + if !ok { + return nil, fmt.Errorf("invalid date") + } + date, err := time.Parse(time.RFC3339, dateStr) if err != nil { - return fmt.Errorf("error deleting existing NIP-88 event: %v", err) + return nil, fmt.Errorf("error parsing date: %v", err) } - // Create a new event with updated status - newEvent := *existingEvent - newEvent.CreatedAt = nostr.Timestamp(time.Now().Unix()) - - // Update the tags - for i, tag := range newEvent.Tags { - switch tag[0] { - case "subscription_status": - newEvent.Tags[i] = nostr.Tag{"subscription_status", "active"} - case "active_subscription": - newEvent.Tags[i] = nostr.Tag{"active_subscription", tier, fmt.Sprintf("%d", expirationTimestamp)} - } + output, ok := transaction["output"].(string) + if !ok { + return nil, fmt.Errorf("invalid output") } - // Generate new ID and signature - serializedEvent := newEvent.Serialize() - hash := sha256.Sum256(serializedEvent) - newEvent.ID = hex.EncodeToString(hash[:]) - - sig, err := schnorr.Sign(relayPrivKey, hash[:]) - if err != nil { - return fmt.Errorf("error signing updated event: %v", err) + valueStr, ok := transaction["value"].(string) + if !ok { + return nil, fmt.Errorf("invalid value") } - newEvent.Sig = hex.EncodeToString(sig.Serialize()) - - // Store the updated event - err = store.StoreEvent(&newEvent) + value, err := strconv.ParseFloat(valueStr, 64) if err != nil { - return fmt.Errorf("failed to store updated NIP-88 event: %v", err) + return nil, fmt.Errorf("error parsing value: %v", err) } - return nil + return &transactionDetails{ + address: address, + date: date, + output: output, + value: value, + valueStr: valueStr, + }, nil } -func getExistingNIP88Event(store stores.Store, userPubKey string) (*nostr.Event, error) { - filter := nostr.Filter{ - Kinds: []int{88}, - Tags: nostr.TagMap{ - "p": []string{userPubKey}, - }, - Limit: 1, - } - - events, err := store.QueryEvents(filter) - if err != nil { - return nil, err - } +// validateWalletName ensures the wallet name is valid and consistent +func validateWalletName(transactions []map[string]interface{}) string { + expectedWalletName := viper.GetString("wallet_name") - if len(events) > 0 { - return events[0], nil + // Set wallet name from first transaction if not set + if expectedWalletName == "" && len(transactions) > 0 { + if walletName, ok := transactions[0]["wallet_name"].(string); ok { + viper.Set("wallet_name", walletName) + expectedWalletName = walletName + } } - return nil, nil + return expectedWalletName } -func loadRelayPrivateKey() (*btcec.PrivateKey, *btcec.PublicKey, error) { - - privateKey, publicKey, err := signing.DeserializePrivateKey(viper.GetString("priv_key")) +// initializeSubscriptionManager creates a new subscription manager instance +func initializeSubscriptionManager(store stores.Store) (*subscription.SubscriptionManager, error) { + // Load relay private key + privateKey, _, err := signing.DeserializePrivateKey(viper.GetString("private_key")) if err != nil { - return nil, nil, fmt.Errorf("error getting keys: %s", err) + return nil, fmt.Errorf("failed to load relay private key: %v", err) } - return privateKey, publicKey, nil + // Get subscription tiers from config + var subscriptionTiers []types.SubscriptionTier + if err := viper.UnmarshalKey("subscription_tiers", &subscriptionTiers); err != nil { + return nil, fmt.Errorf("failed to load subscription tiers: %v", err) + } + + // Create and return the subscription manager + return subscription.NewSubscriptionManager( + store, + privateKey, + viper.GetString("RelayDHTkey"), + subscriptionTiers, + ), nil } + +// package web + +// import ( +// "crypto/sha256" +// "encoding/hex" +// "fmt" +// "log" +// "strconv" +// "strings" +// "time" + +// "github.com/btcsuite/btcd/btcec/v2" +// "github.com/btcsuite/btcd/btcec/v2/schnorr" +// "github.com/gofiber/fiber/v2" +// "github.com/nbd-wtf/go-nostr" +// "github.com/spf13/viper" + +// types "github.com/HORNET-Storage/hornet-storage/lib" +// "github.com/HORNET-Storage/hornet-storage/lib/signing" +// "github.com/HORNET-Storage/hornet-storage/lib/stores" +// ) + +// func updateWalletTransactions(c *fiber.Ctx, store stores.Store) error { +// var transactions []map[string]interface{} +// log.Println("Transactions request received") + +// // Parse the JSON body into the slice of maps +// if err := c.BodyParser(&transactions); err != nil { +// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ +// "error": "Cannot parse JSON", +// }) +// } + +// // Get the expected wallet name from the configuration +// expectedWalletName := viper.GetString("wallet_name") + +// // Set wallet name from first transaction if not set +// if expectedWalletName == "" && len(transactions) > 0 { +// walletName, ok := transactions[0]["wallet_name"].(string) +// if !ok { +// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ +// "error": "Wallet name missing or invalid", +// }) +// } +// viper.Set("wallet_name", walletName) +// expectedWalletName = walletName +// } + +// for _, transaction := range transactions { +// walletName, ok := transaction["wallet_name"].(string) +// if !ok || walletName != expectedWalletName { +// continue // Skip processing if wallet name doesn't match +// } + +// // Extract transaction details +// address, _ := transaction["address"].(string) +// dateStr, _ := transaction["date"].(string) +// date, err := time.Parse(time.RFC3339, dateStr) +// if err != nil { +// log.Printf("Error parsing date: %v", err) +// continue +// } +// output, _ := transaction["output"].(string) +// valueStr, _ := transaction["value"].(string) +// value, err := strconv.ParseFloat(valueStr, 64) +// if err != nil { +// log.Printf("Error parsing value: %v", err) +// continue +// } + +// // Process pending transactions +// txID := strings.Split(address, ":")[0] +// err = store.GetStatsStore().DeletePendingTransaction(txID) +// if err != nil { +// continue +// } + +// // Check for existing transactions +// exists, err := store.GetStatsStore().TransactionExists(address, date, output, valueStr) +// if err != nil { +// log.Printf("Error checking existing transactions: %v", err) +// continue +// } +// if exists { +// continue +// } + +// // Create a new transaction +// newTransaction := types.WalletTransactions{ +// Address: address, +// Date: date, +// Output: output, +// Value: fmt.Sprintf("%.8f", value), +// } + +// err = store.GetStatsStore().SaveWalletTransaction(newTransaction) +// if err != nil { +// log.Printf("Error saving new transaction: %v", err) +// continue +// } + +// // Process subscription payments +// err = processSubscriptionPayment(store, transaction) +// if err != nil { +// log.Printf("Error processing subscription payment: %v", err) +// } +// } + +// return c.JSON(fiber.Map{ +// "status": "success", +// "message": "Transactions processed successfully", +// }) +// } + +// func processSubscriptionPayment(store stores.Store, transaction map[string]interface{}) error { +// log.Printf("Processing transaction: %+v", transaction) + +// // Get subscription tiers from config +// var subscriptionTiers []types.SubscriptionTier +// if err := viper.UnmarshalKey("subscription_tiers", &subscriptionTiers); err != nil { +// return fmt.Errorf("failed to fetch subscription tiers: %v", err) +// } + +// // Log subscription tiers to confirm they are loaded correctly +// for _, tier := range subscriptionTiers { +// log.Printf("Loaded subscription tier: DataLimit=%s, Price=%s", tier.DataLimit, tier.Price) +// } + +// // Extract and validate the Bitcoin address +// output, ok := transaction["output"].(string) +// if !ok { +// return fmt.Errorf("invalid output in transaction") +// } +// log.Printf("Looking up subscriber for address: %s", output) + +// // Debug: Check if address exists in subscriber_addresses table +// if store, ok := store.(interface { +// AddressExists(string) (bool, error) +// }); ok { +// exists, err := store.AddressExists(output) +// if err != nil { +// log.Printf("Error checking address existence: %v", err) +// } else { +// log.Printf("Address %s exists in database: %v", output, exists) +// } +// } + +// // Debug: Check address allocation status +// if store, ok := store.(interface { +// DebugAddressDetails(string) +// }); ok { +// log.Printf("Checking address details for: %s", output) +// store.DebugAddressDetails(output) +// } + +// // Get subscriber details +// subscriber, err := store.GetSubscriberByAddress(output) +// if err != nil { +// log.Printf("Failed to find subscriber for address %s: %v", output, err) +// // Dump the subscriber_addresses table contents for debugging +// if store, ok := store.(interface { +// DumpAddressTable() +// }); ok { +// store.DumpAddressTable() +// } +// return fmt.Errorf("subscriber not found: %v", err) +// } + +// // Validate and parse transaction details +// transactionID, valueStr, err := validateTransactionDetails(transaction, subscriber) +// if err != nil { +// return err +// } + +// // Find matching tier for payment amount +// matchedTier, err := findMatchingTier(valueStr, subscriptionTiers) +// if err != nil { +// return err +// } +// if matchedTier == nil { +// log.Printf("Transaction value %v does not match any subscription tier for address: %s", valueStr, output) +// return nil +// } + +// // Log to confirm the DataLimit value +// log.Printf("Matched tier data limit: %s", matchedTier.DataLimit) + +// // Update subscription details +// newEndDate := calculateNewEndDate(subscriber.EndDate) +// storageLimitBytes, err := stores.ParseStorageLimit(matchedTier.DataLimit) +// if err != nil { +// return fmt.Errorf("failed to parse storage limit: %v", err) +// } + +// // Update subscriber record +// if err := updateSubscriberRecord(store, subscriber, matchedTier, transactionID, newEndDate, storageLimitBytes); err != nil { +// return err +// } + +// // Update NIP-88 event +// if err := updateNIP88Event(store, subscriber, matchedTier, newEndDate); err != nil { +// log.Printf("Warning: NIP-88 event update failed: %v", err) +// // Continue despite NIP-88 error as the subscription is already updated +// } + +// log.Printf("Subscriber %s activated/extended on tier %s with transaction ID %s. New end date: %v", +// subscriber.Npub, matchedTier.DataLimit, transactionID, newEndDate) + +// return nil +// } + +// func validateTransactionDetails(transaction map[string]interface{}, subscriber *types.Subscriber) (string, string, error) { +// txAddressField, ok := transaction["address"].(string) +// if !ok || txAddressField == "" { +// return "", "", fmt.Errorf("transaction ID missing or invalid") +// } +// transactionID := strings.Split(txAddressField, ":")[0] + +// if subscriber.LastTransactionID == transactionID { +// log.Printf("Transaction ID %s has already been processed for subscriber %s", transactionID, subscriber.Npub) +// return "", "", fmt.Errorf("transaction already processed") +// } + +// valueStr, ok := transaction["value"].(string) +// if !ok { +// return "", "", fmt.Errorf("transaction value missing or invalid") +// } + +// return transactionID, valueStr, nil +// } + +// func findMatchingTier(valueStr string, tiers []types.SubscriptionTier) (*types.SubscriptionTier, error) { +// // Parse the BTC value as a float +// value, err := strconv.ParseFloat(valueStr, 64) +// if err != nil { +// return nil, fmt.Errorf("error parsing transaction value: %v", err) +// } + +// // Convert the value from BTC to satoshis (1 BTC = 100,000,000 satoshis) +// paymentSats := int64(value * 100_000_000) +// log.Printf("Processing payment of %d satoshis", paymentSats) + +// var bestMatch *types.SubscriptionTier +// var bestPrice int64 = 0 + +// for _, tier := range tiers { +// log.Printf("Checking tier: DataLimit=%s, Price=%s", tier.DataLimit, tier.Price) + +// // Parse the tier price as an integer in satoshis +// tierPrice, err := strconv.ParseInt(tier.Price, 10, 64) +// if err != nil { +// log.Printf("Warning: invalid tier price configuration: %v", err) +// continue +// } + +// // Check if the payment meets or exceeds the tier price, and if it’s the highest eligible price +// if paymentSats >= tierPrice && tierPrice > bestPrice { +// tierCopy := tier // Copy the struct to avoid pointer issues +// bestMatch = &tierCopy +// bestPrice = tierPrice +// log.Printf("Found matching tier: %s (price: %d sats)", tier.DataLimit, tierPrice) +// } +// } + +// if bestMatch != nil { +// log.Printf("Selected tier: %s for payment of %d satoshis", bestMatch.DataLimit, paymentSats) +// } else { +// log.Printf("No matching tier found for payment of %d satoshis", paymentSats) +// } + +// return bestMatch, nil +// } + +// func calculateNewEndDate(currentEndDate time.Time) time.Time { +// if time.Now().Before(currentEndDate) { +// return currentEndDate.AddDate(0, 1, 0) +// } +// return time.Now().AddDate(0, 1, 0) +// } + +// func updateSubscriberRecord(store stores.Store, subscriber *types.Subscriber, tier *types.SubscriptionTier, +// transactionID string, endDate time.Time, storageLimitBytes int64) error { + +// log.Println("Updating subscriber: ", subscriber.Npub) +// subscriber.Tier = tier.DataLimit +// subscriber.StartDate = time.Now() +// subscriber.EndDate = endDate +// subscriber.LastTransactionID = transactionID + +// if subscriberStore, ok := store.(stores.SubscriberStore); ok { +// period := &types.SubscriptionPeriod{ +// TransactionID: transactionID, +// Tier: tier.DataLimit, +// StorageLimitBytes: storageLimitBytes, +// StartDate: time.Now(), +// EndDate: endDate, +// PaymentAmount: tier.Price, +// } +// if err := subscriberStore.AddSubscriptionPeriod(subscriber.Npub, period); err != nil { +// return fmt.Errorf("failed to add subscription period: %v", err) +// } +// } + +// err := store.DeleteSubscriber(subscriber.Npub) +// if err != nil { +// return fmt.Errorf("failed to delete subscriber: %v", err) +// } + +// newSubscriberEntry := &types.Subscriber{ +// Npub: subscriber.Npub, +// Tier: tier.DataLimit, +// StartDate: time.Now(), +// EndDate: endDate, +// Address: subscriber.Address, +// LastTransactionID: transactionID, +// } + +// return store.SaveSubscriber(newSubscriberEntry) +// } + +// func updateNIP88Event(store stores.Store, subscriber *types.Subscriber, tier *types.SubscriptionTier, endDate time.Time) error { +// relayPrivKey, _, err := loadRelayPrivateKey() +// if err != nil { +// return fmt.Errorf("failed to load relay private key: %v", err) +// } + +// return UpdateNIP88EventAfterPayment(relayPrivKey, subscriber.Npub, store, tier.DataLimit, endDate.Unix()) +// } + +// func UpdateNIP88EventAfterPayment(relayPrivKey *btcec.PrivateKey, userPubKey string, store stores.Store, tier string, expirationTimestamp int64) error { +// existingEvent, err := getExistingNIP88Event(store, userPubKey) +// if err != nil { +// return fmt.Errorf("error fetching existing NIP-88 event: %v", err) +// } +// if existingEvent == nil { +// return fmt.Errorf("no existing NIP-88 event found for user") +// } + +// // Delete the existing event +// err = store.DeleteEvent(existingEvent.ID) +// if err != nil { +// return fmt.Errorf("error deleting existing NIP-88 event: %v", err) +// } + +// subscriptionTiers := []types.SubscriptionTier{ +// {DataLimit: "1 GB per month", Price: "8000"}, +// {DataLimit: "5 GB per month", Price: "10000"}, +// {DataLimit: "10 GB per month", Price: "15000"}, +// } + +// var relayAddress string +// for _, tag := range existingEvent.Tags { +// if tag[0] == "relay_bitcoin_address" && len(tag) > 1 { +// relayAddress = tag[1] +// break +// } +// } + +// tags := []nostr.Tag{ +// {"subscription_duration", "1 month"}, +// {"p", userPubKey}, +// {"subscription_status", "active"}, +// {"relay_bitcoin_address", relayAddress}, +// {"relay_dht_key", viper.GetString("RelayDHTkey")}, +// {"active_subscription", tier, fmt.Sprintf("%d", expirationTimestamp)}, +// } + +// for _, tier := range subscriptionTiers { +// tags = append(tags, nostr.Tag{"subscription-tier", tier.DataLimit, tier.Price}) +// } + +// serializedPrivateKey, err := signing.SerializePrivateKey(relayPrivKey) +// if err != nil { +// log.Printf("failed to serialize private key") +// } + +// event := &nostr.Event{ +// PubKey: *serializedPrivateKey, +// CreatedAt: nostr.Timestamp(time.Now().Unix()), +// Kind: 764, +// Tags: tags, +// Content: "", +// } + +// // Generate the event ID +// serializedEvent := event.Serialize() +// hash := sha256.Sum256(serializedEvent) +// event.ID = hex.EncodeToString(hash[:]) + +// // Sign the event +// sig, err := schnorr.Sign(relayPrivKey, hash[:]) +// if err != nil { +// return fmt.Errorf("error signing event: %v", err) +// } +// event.Sig = hex.EncodeToString(sig.Serialize()) + +// log.Println("Storing updated kind 764 event") + +// // Store the event +// err = store.StoreEvent(event) +// if err != nil { +// return fmt.Errorf("failed to store NIP-88 event: %v", err) +// } + +// log.Println("Kind 764 event successfully stored.") + +// return nil +// } + +// func getExistingNIP88Event(store stores.Store, userPubKey string) (*nostr.Event, error) { +// filter := nostr.Filter{ +// Kinds: []int{764}, +// Tags: nostr.TagMap{ +// "p": []string{userPubKey}, +// }, +// Limit: 1, +// } + +// events, err := store.QueryEvents(filter) +// if err != nil { +// return nil, err +// } + +// if len(events) > 0 { +// return events[0], nil +// } + +// return nil, nil +// } + +// func loadRelayPrivateKey() (*btcec.PrivateKey, *btcec.PublicKey, error) { +// privateKey, publicKey, err := signing.DeserializePrivateKey(viper.GetString("priv_key")) +// if err != nil { +// return nil, nil, fmt.Errorf("error getting keys: %s", err) +// } + +// return privateKey, publicKey, nil +// } diff --git a/lib/web/handler_wallet_addresses.go b/lib/web/handler_wallet_addresses.go index ade74cd..f3960d8 100644 --- a/lib/web/handler_wallet_addresses.go +++ b/lib/web/handler_wallet_addresses.go @@ -19,11 +19,13 @@ const ( AddressStatusUsed = "used" ) +// saveWalletAddresses processes incoming Bitcoin addresses and stores them for future +// subscriber allocation. These addresses will be used when subscribers initialize their +// subscription and need a payment address. func saveWalletAddresses(c *fiber.Ctx, store stores.Store) error { log.Println("Addresses request received") body := c.Body() - log.Println("Raw JSON Body:", string(body)) var addresses []types.Address if err := json.Unmarshal(body, &addresses); err != nil { @@ -33,65 +35,99 @@ func saveWalletAddresses(c *fiber.Ctx, store stores.Store) error { }) } - log.Println("Addresses: ", addresses) - - // Get the expected wallet name from the configuration expectedWalletName := viper.GetString("wallet_name") if expectedWalletName == "" { - log.Println("No expected wallet name set in configuration.") - return c.Status(fiber.StatusInternalServerError).SendString("Internal Server Error") + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Wallet name not configured", + }) } - // Process each address + log.Printf("Expected wallet name: %s", expectedWalletName) + + statsStore := store.GetStatsStore() + if statsStore == nil { + log.Println("Error: StatsStore is nil or not initialized") + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "StatsStore not available", + }) + } + log.Println("Successfully accessed StatsStore") + + log.Println("Successfully accessed SubscriberStore") + + processedCount := 0 + + // Process each address from the request for _, addr := range addresses { - // Check if the wallet name matches the expected one if addr.WalletName != expectedWalletName { - log.Printf("Address from unknown wallet: %s, skipping.", addr.WalletName) + log.Printf("Skipping address from unknown wallet: %s", addr.WalletName) continue } - // Check if the address already exists in the SQL database using the store method - addressExists, err := store.GetStatsStore().AddressExists(addr.Address) + // Check if the address exists in StatsStore and save if it doesn't + existsInStatsStore, err := statsStore.AddressExists(addr.Address) if err != nil { - log.Printf("Error checking if address exists: %v", err) - continue - } - - if addressExists { - log.Printf("Duplicate address found, skipping: %s", addr.Address) + log.Printf("Error checking address existence in StatsStore: %v", err) continue } - // Create a new address in the SQL database using the store method - newAddress := types.WalletAddress{ - Index: addr.Index, - Address: addr.Address, + // Save address to StatsStore if it doesn't exist + if !existsInStatsStore { + newStatsAddress := types.WalletAddress{ + Index: addr.Index, + Address: addr.Address, + } + log.Printf("Attempting to save new address to StatsStore: %v", newStatsAddress) + if err := statsStore.SaveAddress(&newStatsAddress); err != nil { + log.Printf("Error saving new address to StatsStore: %v", err) + continue + } + log.Printf("Address saved to StatsStore: %v", newStatsAddress) } - if err := store.GetStatsStore().SaveAddress(&newAddress); err != nil { - log.Printf("Error saving new address: %v", err) + // Check if the address exists in SubscriberStore and save if it doesn't + existsInSubscriberStore, err := store.GetSubscriberStore().AddressExists(addr.Address) + if err != nil { + log.Printf("Error checking address existence in SubscriberStore: %v", err) continue } - // Add the address to the Graviton store - // Add the address to the Graviton store with default values - subscriptionAddress := &types.SubscriberAddress{ - Index: fmt.Sprint(addr.Index), - Address: addr.Address, - WalletName: addr.WalletName, - Status: AddressStatusAvailable, // Default status - AllocatedAt: &time.Time{}, // Use zero time if not allocated - Npub: "", // Default to empty string + // Save WalletAddress to SubscriberStore if it doesn't exist + if !existsInSubscriberStore { + newSubscriberAddress := types.WalletAddress{ + Index: addr.Index, + Address: addr.Address, + } + log.Printf("Attempting to save new WalletAddress to SubscriberStore: %v", newSubscriberAddress) + if err := store.GetSubscriberStore().SaveSubscriberAddresses(&newSubscriberAddress); err != nil { + log.Printf("Error saving WalletAddress to SubscriberStore: %v", err) + continue + } + log.Printf("WalletAddress saved to SubscriberStore: %v", newSubscriberAddress) + + // Save Subscriber-specific data to SubscriberStore + subscriptionAddress := &types.SubscriberAddress{ + Index: fmt.Sprint(addr.Index), + Address: addr.Address, + WalletName: addr.WalletName, + Status: AddressStatusAvailable, + AllocatedAt: &time.Time{}, + Npub: "", // Use nil for empty pointer + } + log.Printf("Attempting to save SubscriberAddress to SubscriberStore: %v", subscriptionAddress) + if err := store.GetSubscriberStore().SaveSubscriberAddress(subscriptionAddress); err != nil { + log.Printf("Error saving SubscriberAddress to SubscriberStore: %v", err) + continue + } + log.Printf("SubscriberAddress saved to SubscriberStore: %v", subscriptionAddress) } - if err := store.GetStatsStore().SaveSubcriberAddress(subscriptionAddress); err != nil { - log.Printf("Error saving address to Graviton store: %v", err) - } + processedCount++ } - // Respond with a success message + // Return success response with number of addresses processed return c.JSON(fiber.Map{ "status": "success", - "message": "Addresses received and processed successfully", + "message": fmt.Sprintf("Processed %d addresses successfully", processedCount), }) } diff --git a/services/server/port/main.go b/services/server/port/main.go index 0478984..5cb36cc 100644 --- a/services/server/port/main.go +++ b/services/server/port/main.go @@ -77,6 +77,7 @@ func init() { viper.SetDefault("proxy", true) viper.SetDefault("port", "9000") viper.SetDefault("relay_stats_db", "relay_stats.db") + viper.SetDefault("subscriber_db", "subscriber_db.db") viper.SetDefault("query_cache", map[string]string{}) viper.SetDefault("service_tag", "hornet-storage-service") viper.SetDefault("RelayName", "HORNETS") From 39bec604ceb873527d91e105667819285b383690 Mon Sep 17 00:00:00 2001 From: Maphikza Date: Thu, 7 Nov 2024 10:28:06 +0200 Subject: [PATCH 11/21] working version for subscription with nip 88 event 764, pre clean up --- lib/handlers/blossom/blossom.go | 59 +- lib/handlers/scionic/upload/upload.go | 52 +- lib/handlers/scionic/utils.go | 124 +++ lib/stores/graviton/graviton.go | 50 -- .../stats_stores/statistics_store_gorm.go | 4 +- lib/stores/subscriber_store.go | 52 +- .../subscription_store_gorm.go | 583 +------------ lib/subscription/subscription.go | 801 +++++------------- lib/types.go | 11 +- lib/web/handler_wallet_addresses.go | 13 +- 10 files changed, 405 insertions(+), 1344 deletions(-) diff --git a/lib/handlers/blossom/blossom.go b/lib/handlers/blossom/blossom.go index 457b64f..4730a94 100644 --- a/lib/handlers/blossom/blossom.go +++ b/lib/handlers/blossom/blossom.go @@ -5,9 +5,8 @@ import ( "encoding/hex" "fmt" "log" - "time" - types "github.com/HORNET-Storage/hornet-storage/lib" + utils "github.com/HORNET-Storage/hornet-storage/lib/handlers/scionic" "github.com/HORNET-Storage/hornet-storage/lib/stores" "github.com/gofiber/fiber/v2" "github.com/nbd-wtf/go-nostr" @@ -38,13 +37,8 @@ func (s *Server) getBlob(c *fiber.Ctx) error { func (s *Server) uploadBlob(c *fiber.Ctx) error { pubkey := c.Query("pubkey") - subscriber, err := s.storage.GetSubscriber(pubkey) - if err != nil { - return err - } - - // Validate subscription status and storage quota - if err := validateUploadEligibility(s.storage, subscriber, c.Body()); err != nil { + // Validate subscription status and storage quota using NIP-88 + if err := utils.ValidateUploadEligibility(s.storage, pubkey, c.Body()); err != nil { log.Printf("Upload validation failed for subscriber %s: %v", pubkey, err) return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ "message": err.Error(), @@ -53,32 +47,36 @@ func (s *Server) uploadBlob(c *fiber.Ctx) error { data := c.Body() + // Compute the hash of the data checkHash := sha256.Sum256(data) encodedHash := hex.EncodeToString(checkHash[:]) + // Filter to find matching events with the computed hash filter := nostr.Filter{ - Kinds: []int{117}, + Kinds: []int{117}, // Assuming 117 is the correct kind for blossom events Authors: []string{pubkey}, Tags: nostr.TagMap{"blossom_hash": []string{encodedHash}}, } - fmt.Println("Recieved a blossom blob") + fmt.Println("Received a blossom blob") + // Query for events matching the filter events, err := s.storage.QueryEvents(filter) if err != nil { return err } - var event *nostr.Event + // Handle case where no matching events are found if len(events) <= 0 { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": "no events match this file upload"}) } - event = events[0] + event := events[0] + // Extract the "blossom_hash" tag value from the event fileHash := event.Tags.GetFirst([]string{"blossom_hash"}) - // Check the submitted hash matches the data being submitted + // Check if the submitted hash matches the expected value from the event if encodedHash != fileHash.Value() { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": "submitted hex encoded hash does not match hex encoded hash of data"}) } @@ -93,36 +91,3 @@ func (s *Server) uploadBlob(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) } - -// validateUploadEligibility checks if the subscriber can upload the file -func validateUploadEligibility(store stores.Store, subscriber *types.Subscriber, data []byte) error { - // Check subscription expiration - if time.Now().After(subscriber.EndDate) { - return fmt.Errorf("subscription expired on %s", subscriber.EndDate.Format(time.RFC3339)) - } - - // Try to use subscriber store features if available - subscriberStore, ok := store.(stores.SubscriberStore) - if !ok { - // Fallback to basic validation if subscriber store is not available - return nil - } - - // Check storage quota - fileSize := int64(len(data)) - if err := subscriberStore.CheckStorageAvailability(subscriber.Npub, fileSize); err != nil { - // Get current usage for detailed error message - stats, statsErr := subscriberStore.GetSubscriberStorageStats(subscriber.Npub) - if statsErr != nil { - return fmt.Errorf("storage quota exceeded") - } - - return fmt.Errorf("storage quota exceeded: used %d of %d bytes (%.2f%%), attempting to upload %d bytes", - stats.CurrentUsageBytes, - stats.StorageLimitBytes, - stats.UsagePercentage, - fileSize) - } - - return nil -} diff --git a/lib/handlers/scionic/upload/upload.go b/lib/handlers/scionic/upload/upload.go index 0e5ba17..c419472 100644 --- a/lib/handlers/scionic/upload/upload.go +++ b/lib/handlers/scionic/upload/upload.go @@ -2,7 +2,6 @@ package upload import ( "context" - "fmt" "log" "github.com/gofiber/contrib/websocket" @@ -83,30 +82,10 @@ func BuildUploadStreamHandler(store stores.Store, canUploadDag func(rootLeaf *me return } - // Add subscription and storage validation here - if subscriberStore, ok := store.(stores.SubscriberStore); ok { - // Get subscriber info - _, err := subscriberStore.GetSubscriber(message.PublicKey) - if err != nil { - write(utils.BuildErrorMessage("Invalid or inactive subscription", err)) - return - } - - // Pre-validate storage quota - // Note: We estimate total size based on the leaf count and average leaf size - estimatedSize := int64(len(message.Leaf.Content)) - if err := subscriberStore.CheckStorageAvailability(message.PublicKey, estimatedSize); err != nil { - stats, _ := subscriberStore.GetSubscriberStorageStats(message.PublicKey) - if stats != nil { - write(utils.BuildErrorMessage(fmt.Sprintf("Storage quota exceeded: used %d of %d bytes (%.2f%%)", - stats.CurrentUsageBytes, - stats.StorageLimitBytes, - stats.UsagePercentage), nil)) - } else { - write(utils.BuildErrorMessage("Storage quota exceeded", nil)) - } - return - } + // Add subscription and storage validation here using NIP-88 validation and update + if err := utils.ValidateUploadEligibility(store, message.PublicKey, message.Leaf.Content); err != nil { + write(utils.BuildErrorMessage("Invalid or inactive subscription", err)) + return } err = message.Leaf.VerifyRootLeaf() @@ -200,29 +179,6 @@ func BuildUploadStreamHandler(store stores.Store, canUploadDag func(rootLeaf *me return } - // Update storage usage after successful DAG upload - if subscriberStore, ok := store.(stores.SubscriberStore); ok { - dagJson, err := dagData.Dag.ToJSON() // Get actual size after DAG is built - if err != nil { - log.Printf("Warning: failed to marshall dag to json: %s", err) - } - actualSize := int64(len(dagJson)) - if err := subscriberStore.UpdateStorageUsage(message.PublicKey, actualSize); err != nil { - log.Printf("Warning: Failed to update storage usage for %s: %v", message.PublicKey, err) - // Continue despite tracking failure as DAG is already stored - } - - // Track the upload - upload := &types.FileUpload{ - Npub: message.PublicKey, - FileHash: message.Root, - SizeBytes: actualSize, - } - if err := subscriberStore.TrackFileUpload(upload); err != nil { - log.Printf("Warning: Failed to track file upload for %s: %v", message.PublicKey, err) - } - } - handleRecievedDag(&dagData.Dag, &message.PublicKey) } diff --git a/lib/handlers/scionic/utils.go b/lib/handlers/scionic/utils.go index 1625128..fc39dc7 100644 --- a/lib/handlers/scionic/utils.go +++ b/lib/handlers/scionic/utils.go @@ -1,6 +1,8 @@ package scionic import ( + "crypto/sha256" + "encoding/hex" "fmt" "io" "log" @@ -11,11 +13,13 @@ import ( "time" "github.com/fxamacker/cbor/v2" + "github.com/nbd-wtf/go-nostr" "github.com/spf13/viper" merkle_dag "github.com/HORNET-Storage/scionic-merkletree/dag" types "github.com/HORNET-Storage/hornet-storage/lib" + "github.com/HORNET-Storage/hornet-storage/lib/stores" ) type DagWriter func(message interface{}) error @@ -214,3 +218,123 @@ func LoadRelaySettings() (*types.RelaySettings, error) { return &settings, nil } + +func ValidateUploadEligibility(store stores.Store, npub string, data []byte) error { + // Step 1: Fetch the NIP-88 event for the given subscriber + events, err := store.QueryEvents(nostr.Filter{ + Kinds: []int{764}, // Assuming 764 is the NIP-88 kind + Tags: nostr.TagMap{ + "p": []string{npub}, + }, + Limit: 1, + }) + if err != nil || len(events) == 0 { + return fmt.Errorf("no NIP-88 event found for user %s", npub) + } + + currentEvent := events[0] + + // Step 2: Extract storage information from the NIP-88 event + storageInfo, err := ExtractStorageInfoFromEvent(currentEvent) + if err != nil { + return fmt.Errorf("failed to extract storage info: %v", err) + } + + // Step 3: Check if there is enough storage available + fileSize := int64(len(data)) + newUsage := storageInfo.UsedBytes + fileSize + if newUsage > storageInfo.TotalBytes { + return fmt.Errorf("storage quota exceeded: used %d of %d bytes (%.2f%%), attempting to upload %d bytes", + storageInfo.UsedBytes, + storageInfo.TotalBytes, + float64(storageInfo.UsedBytes)/float64(storageInfo.TotalBytes)*100, + fileSize) + } + + // Step 4: Update storage usage in the NIP-88 event + storageInfo.UsedBytes = newUsage + storageInfo.UpdatedAt = time.Now() + + // Step 5: Delete the old NIP-88 event + if err := store.DeleteEvent(currentEvent.ID); err != nil { + return fmt.Errorf("failed to delete old NIP-88 event: %v", err) + } + + // Step 6: Create a new NIP-88 event with updated storage information + updatedEvent := CreateUpdatedNIP88Event(currentEvent, storageInfo) + if err := store.StoreEvent(updatedEvent); err != nil { + return fmt.Errorf("failed to store updated NIP-88 event: %v", err) + } + + return nil +} + +// Helper function to extract storage information from a NIP-88 event +func ExtractStorageInfoFromEvent(event *nostr.Event) (types.StorageInfo, error) { + var info types.StorageInfo + + for _, tag := range event.Tags { + if tag[0] == "storage" && len(tag) >= 4 { + used, err := strconv.ParseInt(tag[1], 10, 64) + if err != nil { + return info, fmt.Errorf("invalid used storage value: %v", err) + } + + total, err := strconv.ParseInt(tag[2], 10, 64) + if err != nil { + return info, fmt.Errorf("invalid total storage value: %v", err) + } + + updated, err := strconv.ParseInt(tag[3], 10, 64) + if err != nil { + return info, fmt.Errorf("invalid update timestamp: %v", err) + } + + info.UsedBytes = used + info.TotalBytes = total + info.UpdatedAt = time.Unix(updated, 0) + return info, nil + } + } + + // Return zero values if no storage tag is found + return types.StorageInfo{ + UsedBytes: 0, + TotalBytes: 0, + UpdatedAt: time.Now(), + }, nil +} + +// Helper function to create an updated NIP-88 event +func CreateUpdatedNIP88Event(oldEvent *nostr.Event, storageInfo types.StorageInfo) *nostr.Event { + // Create new tags for the updated storage information + tags := []nostr.Tag{ + {"storage", fmt.Sprintf("%d", storageInfo.UsedBytes), fmt.Sprintf("%d", storageInfo.TotalBytes), fmt.Sprintf("%d", storageInfo.UpdatedAt.Unix())}, + } + + // Copy other tags from the old event + for _, tag := range oldEvent.Tags { + if tag[0] != "storage" { + tags = append(tags, tag) + } + } + + // Create a new event with the updated tags + newEvent := &nostr.Event{ + PubKey: oldEvent.PubKey, + CreatedAt: nostr.Timestamp(time.Now().Unix()), + Kind: oldEvent.Kind, + Tags: tags, + Content: oldEvent.Content, // Keep the same content, if any + } + + // Generate new event ID and sign the event (assuming signing is required) + serializedEvent := newEvent.Serialize() + hash := sha256.Sum256(serializedEvent) + newEvent.ID = hex.EncodeToString(hash[:]) + + // You would need a signing function here, e.g., using a private key + // For example: signEvent(newEvent, privateKey) + + return newEvent +} diff --git a/lib/stores/graviton/graviton.go b/lib/stores/graviton/graviton.go index fd39be0..e9c1c60 100644 --- a/lib/stores/graviton/graviton.go +++ b/lib/stores/graviton/graviton.go @@ -684,15 +684,6 @@ func (store *GravitonStore) StoreBlob(data []byte, hash []byte, publicKey string store.mu.Lock() defer store.mu.Unlock() - // Check if storage tracking is available - if store.SubscriberStore != nil { - // Pre-check storage availability - fileSize := int64(len(data)) - if err := store.SubscriberStore.CheckStorageAvailability(publicKey, fileSize); err != nil { - return fmt.Errorf("storage quota check failed: %v", err) - } - } - // Load snapshot and get content tree snapshot, err := store.Database.LoadSnapshot(0) if err != nil { @@ -724,34 +715,6 @@ func (store *GravitonStore) StoreBlob(data []byte, hash []byte, publicKey string return fmt.Errorf("failed to commit trees: %v", err) } - // Update storage tracking if available - if store.SubscriberStore != nil { - fileSize := int64(len(data)) - - // Track the file upload - upload := &types.FileUpload{ - Npub: publicKey, - FileHash: encodedHash, - SizeBytes: fileSize, - } - - if err := store.SubscriberStore.TrackFileUpload(upload); err != nil { - log.Printf("Warning: Failed to track file upload for %s: %v", publicKey, err) - // Continue despite tracking failure as data is already stored - } - - // Update storage usage - if err := store.SubscriberStore.UpdateStorageUsage(publicKey, fileSize); err != nil { - log.Printf("Warning: Failed to update storage usage for %s: %v", publicKey, err) - } - - // Log successful upload with storage stats - if stats, err := store.SubscriberStore.GetSubscriberStorageStats(publicKey); err == nil { - log.Printf("Blob stored successfully for %s: size=%d bytes, total_used=%d bytes (%.2f%% of quota)", - publicKey, fileSize, stats.CurrentUsageBytes, stats.UsagePercentage) - } - } - return nil } @@ -1084,11 +1047,6 @@ func (store *GravitonStore) SaveSubscriber(subscriber *types.Subscriber) error { store.mu.Lock() defer store.mu.Unlock() - // Save to GORM first - if err := store.SubscriberStore.SaveSubscriber(subscriber); err != nil { - return fmt.Errorf("failed to save subscriber to GORM: %v", err) - } - // Load the snapshot and get the "subscribers" tree snapshot, err := store.Database.LoadSnapshot(0) if err != nil { @@ -1213,14 +1171,6 @@ func (store *GravitonStore) DeleteSubscriber(npub string) error { store.mu.Lock() defer store.mu.Unlock() - // First try to delete from GORM store if available - if store.SubscriberStore != nil { - if err := store.SubscriberStore.DeleteSubscriber(npub); err != nil { - // Log the error but continue with Graviton deletion - log.Printf("Warning: Failed to delete subscriber from GORM store: %v", err) - } - } - snapshot, err := store.Database.LoadSnapshot(0) if err != nil { return fmt.Errorf("failed to load snapshot: %v", err) diff --git a/lib/stores/stats_stores/statistics_store_gorm.go b/lib/stores/stats_stores/statistics_store_gorm.go index 3a8193c..1853558 100644 --- a/lib/stores/stats_stores/statistics_store_gorm.go +++ b/lib/stores/stats_stores/statistics_store_gorm.go @@ -52,7 +52,7 @@ func (store *GormStatisticsStore) InitStore(basepath string, args ...interface{} &types.Audio{}, &types.PendingTransaction{}, &types.ActiveToken{}, - &types.SubscriberAddress{}, + // &types.SubscriberAddress{}, ) if err != nil { return fmt.Errorf("failed to migrate database schema: %v", err) @@ -844,7 +844,7 @@ func (store *GormStatisticsStore) AllocateBitcoinAddress(npub string) (*types.Ad now := time.Now() subscriptionAddress.Status = AddressStatusAllocated subscriptionAddress.AllocatedAt = &now - subscriptionAddress.Npub = npub + subscriptionAddress.Npub = &npub // Save the updated address if err := tx.Save(&subscriptionAddress).Error; err != nil { diff --git a/lib/stores/subscriber_store.go b/lib/stores/subscriber_store.go index 61e3f28..8a2b34d 100644 --- a/lib/stores/subscriber_store.go +++ b/lib/stores/subscriber_store.go @@ -1,60 +1,16 @@ package stores import ( - "fmt" - - types "github.com/HORNET-Storage/hornet-storage/lib" + "github.com/HORNET-Storage/hornet-storage/lib" ) -// SubscriberStore defines the interface for managing subscribers and their storage usage +// SubscriberStore defines the interface for managing address-related functions type SubscriberStore interface { // Store initialization InitStore(basepath string, args ...interface{}) error - // Subscriber management - SaveSubscriber(subscriber *types.Subscriber) error - GetSubscriber(npub string) (*types.Subscriber, error) - GetSubscriberByAddress(address string) (*types.Subscriber, error) - DeleteSubscriber(npub string) error - ListSubscribers() ([]*types.Subscriber, error) - - // Storage quota and statistics - GetSubscriberStorageStats(npub string) (*types.StorageStats, error) - UpdateStorageUsage(npub string, sizeBytes int64) error - GetStorageUsage(npub string) (*types.StorageUsage, error) - CheckStorageAvailability(npub string, requestedBytes int64) error - - // Subscription periods - AddSubscriptionPeriod(npub string, period *types.SubscriptionPeriod) error - GetSubscriptionPeriods(npub string) ([]*types.SubscriptionPeriod, error) - GetActiveSubscription(npub string) (*types.SubscriptionPeriod, error) - GetSubscriptionByTransactionID(transactionID string) (*types.SubscriptionPeriod, error) - - // File management - TrackFileUpload(upload *types.FileUpload) error - DeleteFile(npub string, fileHash string) error - GetFilesBySubscriber(npub string) ([]*types.FileUpload, error) - GetRecentUploads(npub string, limit int) ([]*types.FileUpload, error) - // Address management - SaveSubscriberAddress(address *types.SubscriberAddress) error - AllocateBitcoinAddress(npub string) (*types.Address, error) + AllocateBitcoinAddress(npub string) (*lib.Address, error) AddressExists(address string) (bool, error) - SaveSubscriberAddresses(address *types.WalletAddress) error -} - -// Convert storage strings to bytes -func ParseStorageLimit(limit string) (int64, error) { - var bytes int64 - switch limit { - case "1 GB per month": - bytes = 1 * 1024 * 1024 * 1024 - case "5 GB per month": - bytes = 5 * 1024 * 1024 * 1024 - case "10 GB per month": - bytes = 10 * 1024 * 1024 * 1024 - default: - return 0, fmt.Errorf("unknown storage limit: %s", limit) - } - return bytes, nil + SaveSubscriberAddress(address *lib.SubscriberAddress) error } diff --git a/lib/stores/subscription_store/subscription_store_gorm.go b/lib/stores/subscription_store/subscription_store_gorm.go index 3186c81..7ac2193 100644 --- a/lib/stores/subscription_store/subscription_store_gorm.go +++ b/lib/stores/subscription_store/subscription_store_gorm.go @@ -10,6 +10,7 @@ import ( "gorm.io/gorm" ) +// GormSubscriberStore provides a GORM-based implementation of the SubscriberStore interface type GormSubscriberStore struct { DB *gorm.DB } @@ -17,25 +18,8 @@ type GormSubscriberStore struct { const ( AddressStatusAvailable = "available" AddressStatusAllocated = "allocated" - AddressStatusUsed = "used" ) -// Convert storage strings to bytes -func parseStorageLimit(limit string) (int64, error) { - var bytes int64 - switch limit { - case "1 GB per month": - bytes = 1 * 1024 * 1024 * 1024 - case "5 GB per month": - bytes = 5 * 1024 * 1024 * 1024 - case "10 GB per month": - bytes = 10 * 1024 * 1024 * 1024 - default: - return 0, fmt.Errorf("unknown storage limit: %s", limit) - } - return bytes, nil -} - // InitStore initializes the GORM subscriber store func (store *GormSubscriberStore) InitStore(basepath string, args ...interface{}) error { var err error @@ -48,11 +32,7 @@ func (store *GormSubscriberStore) InitStore(basepath string, args ...interface{} // Run migrations err = store.DB.AutoMigrate( - &types.GormSubscriber{}, - &types.SubscriptionPeriod{}, - &types.FileUpload{}, &types.SubscriberAddress{}, - &types.WalletAddress{}, ) if err != nil { return fmt.Errorf("failed to run migrations: %v", err) @@ -66,358 +46,38 @@ func NewGormSubscriberStore() *GormSubscriberStore { return &GormSubscriberStore{} } -// SaveSubscriber saves or updates a subscriber -// For new subscribers: -// - Creates with default values (no tier/storage) -// For existing subscribers: -// - Updates with new tier and storage limits if provided -func (store *GormSubscriberStore) SaveSubscriber(subscriber *types.Subscriber) error { - tx := store.DB.Begin() - if tx.Error != nil { - return tx.Error - } - defer func() { - if r := recover(); r != nil { - tx.Rollback() - } - }() - - var gormSubscriber types.GormSubscriber - result := tx.Where("npub = ?", subscriber.Npub).First(&gormSubscriber) - - if result.Error == gorm.ErrRecordNotFound { - // Creating new subscriber - gormSubscriber = types.GormSubscriber{ - Npub: subscriber.Npub, - StorageUsedBytes: 0, - StorageLimitBytes: 0, // No storage limit until subscription - LastUpdated: time.Now(), - } - - // If tier is provided (unusual for new subscriber but possible) - if subscriber.Tier != "" { - storageLimitBytes, err := parseStorageLimit(subscriber.Tier) - if err != nil { - tx.Rollback() - return fmt.Errorf("invalid tier for new subscriber: %v", err) - } - gormSubscriber.CurrentTier = subscriber.Tier - gormSubscriber.StorageLimitBytes = storageLimitBytes - gormSubscriber.StartDate = subscriber.StartDate - gormSubscriber.EndDate = subscriber.EndDate - } - - if err := tx.Create(&gormSubscriber).Error; err != nil { - tx.Rollback() - return fmt.Errorf("failed to create subscriber: %v", err) - } - - log.Printf("Created new subscriber: %s", subscriber.Npub) - } else if result.Error != nil { - tx.Rollback() - return fmt.Errorf("failed to query subscriber: %v", result.Error) - } else { - // Updating existing subscriber - if subscriber.Tier != "" { - // Only update tier-related fields if a tier is provided - storageLimitBytes, err := parseStorageLimit(subscriber.Tier) - if err != nil { - tx.Rollback() - return fmt.Errorf("invalid tier for subscription update: %v", err) - } - - gormSubscriber.CurrentTier = subscriber.Tier - gormSubscriber.StorageLimitBytes = storageLimitBytes - gormSubscriber.StartDate = subscriber.StartDate - gormSubscriber.EndDate = subscriber.EndDate - } - - gormSubscriber.LastUpdated = time.Now() - - if err := tx.Save(&gormSubscriber).Error; err != nil { - tx.Rollback() - return fmt.Errorf("failed to update subscriber: %v", err) - } - - log.Printf("Updated subscriber: %s", subscriber.Npub) - } - - // Only create subscription period if tier and transaction ID are provided - if subscriber.Tier != "" && subscriber.LastTransactionID != "" { - storageLimitBytes, _ := parseStorageLimit(subscriber.Tier) // Error already checked above - subscriptionPeriod := types.SubscriptionPeriod{ - SubscriberID: gormSubscriber.ID, - TransactionID: subscriber.LastTransactionID, - Tier: subscriber.Tier, - StorageLimitBytes: storageLimitBytes, - StartDate: subscriber.StartDate, - EndDate: subscriber.EndDate, - PaymentAmount: "", // Add payment amount when available - } - - if err := tx.Create(&subscriptionPeriod).Error; err != nil { - tx.Rollback() - return fmt.Errorf("failed to create subscription period: %v", err) - } - - log.Printf("Created subscription period for %s: %s tier", - subscriber.Npub, subscriber.Tier) - } - - return tx.Commit().Error -} - -// GetSubscriberStorageStats gets detailed storage statistics -func (store *GormSubscriberStore) GetSubscriberStorageStats(npub string) (*types.StorageStats, error) { - var subscriber types.GormSubscriber - if err := store.DB.Where("npub = ?", npub).First(&subscriber).Error; err != nil { - return nil, err - } - - var recentFiles []types.FileUpload - if err := store.DB.Where("npub = ? AND deleted = ?", npub, false). - Order("created_at desc"). - Limit(10). - Find(&recentFiles).Error; err != nil { - return nil, err - } - - return &types.StorageStats{ - CurrentUsageBytes: subscriber.StorageUsedBytes, - StorageLimitBytes: subscriber.StorageLimitBytes, - UsagePercentage: float64(subscriber.StorageUsedBytes) / float64(subscriber.StorageLimitBytes) * 100, - SubscriptionEnd: subscriber.EndDate, - CurrentTier: subscriber.CurrentTier, - LastUpdated: subscriber.LastUpdated, - RecentFiles: recentFiles, - }, nil -} - -// UpdateStorageUsage updates a subscriber's storage usage -func (store *GormSubscriberStore) UpdateStorageUsage(npub string, sizeBytes int64) error { +func (store *GormSubscriberStore) AllocateBitcoinAddress(npub string) (*types.Address, error) { tx := store.DB.Begin() - if tx.Error != nil { - return tx.Error - } defer func() { if r := recover(); r != nil { tx.Rollback() } }() - var subscriber types.GormSubscriber - if err := tx.Where("npub = ?", npub).First(&subscriber).Error; err != nil { - tx.Rollback() - return err - } - - newUsage := subscriber.StorageUsedBytes + sizeBytes - if newUsage > subscriber.StorageLimitBytes { - tx.Rollback() - return fmt.Errorf("storage limit exceeded: would use %d of %d bytes", newUsage, subscriber.StorageLimitBytes) - } - - if err := tx.Model(&subscriber).Updates(map[string]interface{}{ - "storage_used_bytes": newUsage, - "last_updated": time.Now(), - }).Error; err != nil { + // Step 1: Check if the npub already has an allocated address + var existingAddressRecord types.SubscriberAddress + err := tx.Where("npub = ?", npub).First(&existingAddressRecord).Error + if err == nil { + // If an existing record is found, return it + return &types.Address{ + Index: existingAddressRecord.Index, + Address: existingAddressRecord.Address, + WalletName: existingAddressRecord.WalletName, + Status: existingAddressRecord.Status, + AllocatedAt: existingAddressRecord.AllocatedAt, + Npub: npub, + }, nil + } else if err != gorm.ErrRecordNotFound { + // If another error occurred (not record not found), rollback and return error tx.Rollback() - return err - } - - return tx.Commit().Error -} - -// TrackFileUpload records a new file upload -func (store *GormSubscriberStore) TrackFileUpload(upload *types.FileUpload) error { - return store.DB.Create(upload).Error -} - -// GetSubscriber retrieves a subscriber by npub -func (store *GormSubscriberStore) GetSubscriber(npub string) (*types.Subscriber, error) { - var gormSubscriber types.GormSubscriber - if err := store.DB.Where("npub = ?", npub).First(&gormSubscriber).Error; err != nil { - if err == gorm.ErrRecordNotFound { - // Create a new subscriber with just the npub - newSubscriber := &types.Subscriber{ - Npub: npub, - } - if err := store.SaveSubscriber(newSubscriber); err != nil { - return nil, fmt.Errorf("failed to create new subscriber: %v", err) - } - return newSubscriber, nil - } - return nil, err - } - return gormSubscriber.ToSubscriber(), nil -} - -// DeleteSubscriber removes a subscriber -func (store *GormSubscriberStore) DeleteSubscriber(npub string) error { - return store.DB.Where("npub = ?", npub).Delete(&types.GormSubscriber{}).Error -} - -// ListSubscribers returns all subscribers -func (store *GormSubscriberStore) ListSubscribers() ([]*types.Subscriber, error) { - var gormSubscribers []types.GormSubscriber - if err := store.DB.Find(&gormSubscribers).Error; err != nil { - return nil, err - } - - subscribers := make([]*types.Subscriber, len(gormSubscribers)) - for i, gs := range gormSubscribers { - subscribers[i] = gs.ToSubscriber() - } - return subscribers, nil -} - -// AddSubscriptionPeriod adds a new subscription period -func (store *GormSubscriberStore) AddSubscriptionPeriod(npub string, period *types.SubscriptionPeriod) error { - var subscriber types.GormSubscriber - if err := store.DB.Where("npub = ?", npub).First(&subscriber).Error; err != nil { - return err - } - - period.SubscriberID = subscriber.ID - return store.DB.Create(period).Error -} - -// GetSubscriptionPeriods retrieves all subscription periods for a subscriber -func (store *GormSubscriberStore) GetSubscriptionPeriods(npub string) ([]*types.SubscriptionPeriod, error) { - var subscriber types.GormSubscriber - if err := store.DB.Where("npub = ?", npub).First(&subscriber).Error; err != nil { - return nil, err - } - - var periods []*types.SubscriptionPeriod - if err := store.DB.Where("subscriber_id = ?", subscriber.ID). - Order("start_date desc"). - Find(&periods).Error; err != nil { - return nil, err - } - - return periods, nil -} - -// GetActiveSubscription gets the current active subscription -func (store *GormSubscriberStore) GetActiveSubscription(npub string) (*types.SubscriptionPeriod, error) { - var subscriber types.GormSubscriber - if err := store.DB.Where("npub = ?", npub).First(&subscriber).Error; err != nil { - return nil, err - } - - var period types.SubscriptionPeriod - if err := store.DB.Where("subscriber_id = ? AND end_date > ?", subscriber.ID, time.Now()). - Order("end_date desc"). - First(&period).Error; err != nil { - return nil, err - } - - return &period, nil -} - -func (store *GormSubscriberStore) GetSubscriberByAddress(address string) (*types.Subscriber, error) { - // First find the subscriber_address record with explicit column selection - var subscriberAddress struct { - Address string - Status string - Npub string - } - - err := store.DB.Table("subscriber_addresses"). - Select("address, status, npub"). - Where("address = ?", address). - First(&subscriberAddress).Error - - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, fmt.Errorf("address %s not allocated to any subscriber", address) - } - return nil, fmt.Errorf("error querying address: %v", err) - } - - log.Printf("Found subscriber address record: address=%s, npub=%s, status=%s", - subscriberAddress.Address, subscriberAddress.Npub, subscriberAddress.Status) - - // Verify we have a valid npub - if subscriberAddress.Npub == "" { - return nil, fmt.Errorf("address %s has no associated npub", address) - } - - log.Println("Gorm Subscriber Npub: ", subscriberAddress.Npub) - - // Now get the subscriber using the npub - var subscriber types.GormSubscriber - if err := store.DB.Where("npub = ?", subscriberAddress.Npub).First(&subscriber).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return nil, fmt.Errorf("subscriber not found for npub: %s", subscriberAddress.Npub) - } - return nil, fmt.Errorf("error querying subscriber: %v", err) - } - - // Create and return the subscriber - return &types.Subscriber{ - Npub: subscriber.Npub, - Tier: subscriber.CurrentTier, - StartDate: subscriber.StartDate, - EndDate: subscriber.EndDate, - Address: address, - }, nil -} - -func (store *GormSubscriberStore) SaveSubscriberAddresses(address *types.WalletAddress) error { - return store.DB.Create(address).Error -} - -func (store *GormSubscriberStore) SaveSubscriberAddress(address *types.SubscriberAddress) error { - // Check if the address already exists - var existingAddress types.SubscriberAddress - result := store.DB.Where("address = ?", address.Address).First(&existingAddress) - - if result.Error != nil && result.Error != gorm.ErrRecordNotFound { - log.Printf("Error querying existing address: %v", result.Error) - return result.Error + return nil, fmt.Errorf("failed to query existing address for npub: %v", err) } - // If the address already exists, log and skip the insert - if result.RowsAffected > 0 { - log.Printf("Address %s already exists, skipping save.", address.Address) - return nil - } - - // Set defaults if needed - if address.Status == "" { - address.Status = AddressStatusAvailable - } - if address.AllocatedAt == nil { - now := time.Now() - address.AllocatedAt = &now - } - address.Npub = "" // Explicitly set to NULL - - // Create the new address in the database - if err := store.DB.Create(address).Error; err != nil { - log.Printf("Error saving new address: %v", err) - return err - } - - log.Printf("Address %s saved successfully.", address.Address) - return nil -} - -func (store *GormSubscriberStore) AllocateBitcoinAddress(npub string) (*types.Address, error) { - tx := store.DB.Begin() - defer func() { - if r := recover(); r != nil { - tx.Rollback() - } - }() - - // Modified query to handle NULL npub - var subscriptionAddress types.SubscriberAddress - err := tx.Where("status = ? AND (npub IS NULL OR npub = '')", AddressStatusAvailable). + // Step 2: Allocate a new address if no existing address is found + var addressRecord types.SubscriberAddress + err = tx.Where("status = ? AND (npub IS NULL OR npub = '')", AddressStatusAvailable). Order("id"). - First(&subscriptionAddress).Error + First(&addressRecord).Error if err != nil { tx.Rollback() @@ -427,7 +87,6 @@ func (store *GormSubscriberStore) AllocateBitcoinAddress(npub string) (*types.Ad return nil, fmt.Errorf("failed to query available addresses: %v", err) } - // Update the address fields now := time.Now() updates := map[string]interface{}{ "status": AddressStatusAllocated, @@ -435,7 +94,7 @@ func (store *GormSubscriberStore) AllocateBitcoinAddress(npub string) (*types.Ad "npub": npub, } - if err := tx.Model(&subscriptionAddress).Updates(updates).Error; err != nil { + if err := tx.Model(&addressRecord).Updates(updates).Error; err != nil { tx.Rollback() return nil, fmt.Errorf("failed to update address allocation: %v", err) } @@ -445,15 +104,16 @@ func (store *GormSubscriberStore) AllocateBitcoinAddress(npub string) (*types.Ad } return &types.Address{ - Index: subscriptionAddress.Index, - Address: subscriptionAddress.Address, - WalletName: subscriptionAddress.WalletName, - Status: subscriptionAddress.Status, - AllocatedAt: subscriptionAddress.AllocatedAt, + Index: addressRecord.Index, + Address: addressRecord.Address, + WalletName: addressRecord.WalletName, + Status: addressRecord.Status, + AllocatedAt: addressRecord.AllocatedAt, Npub: npub, }, nil } +// AddressExists checks if a given Bitcoin address exists in the database func (store *GormSubscriberStore) AddressExists(address string) (bool, error) { var count int64 err := store.DB.Model(&types.SubscriberAddress{}). @@ -467,180 +127,31 @@ func (store *GormSubscriberStore) AddressExists(address string) (bool, error) { return count > 0, nil } -// GetSubscriptionByTransactionID retrieves a subscription by its transaction ID -func (store *GormSubscriberStore) GetSubscriptionByTransactionID(transactionID string) (*types.SubscriptionPeriod, error) { - var period types.SubscriptionPeriod - if err := store.DB.Where("transaction_id = ?", transactionID).First(&period).Error; err != nil { - return nil, err - } - return &period, nil -} - -// CheckStorageAvailability verifies if the subscriber has enough storage space -func (store *GormSubscriberStore) CheckStorageAvailability(npub string, requestedBytes int64) error { - var subscriber types.GormSubscriber - if err := store.DB.Where("npub = ?", npub).First(&subscriber).Error; err != nil { - return err - } - - // If no tier/storage limit is set, they can't upload - if subscriber.StorageLimitBytes == 0 { - return fmt.Errorf("no active subscription: storage limit not set") - } - - if subscriber.EndDate.IsZero() || time.Now().After(subscriber.EndDate) { - return fmt.Errorf("subscription expired or not yet activated") - } - - newUsage := subscriber.StorageUsedBytes + requestedBytes - if newUsage > subscriber.StorageLimitBytes { - return fmt.Errorf("storage limit exceeded: would use %d of %d bytes", - newUsage, subscriber.StorageLimitBytes) - } - - return nil -} - -// GetStorageUsage gets current storage usage -func (store *GormSubscriberStore) GetStorageUsage(npub string) (*types.StorageUsage, error) { - var subscriber types.GormSubscriber - if err := store.DB.Where("npub = ?", npub).First(&subscriber).Error; err != nil { - return nil, err - } - - return &types.StorageUsage{ - Npub: subscriber.Npub, - UsedBytes: subscriber.StorageUsedBytes, - AllocatedBytes: subscriber.StorageLimitBytes, - LastUpdated: subscriber.LastUpdated, - }, nil -} - -// GetFilesBySubscriber retrieves all files for a subscriber -func (store *GormSubscriberStore) GetFilesBySubscriber(npub string) ([]*types.FileUpload, error) { - var files []*types.FileUpload - if err := store.DB.Where("npub = ? AND deleted = ?", npub, false). - Order("created_at desc"). - Find(&files).Error; err != nil { - return nil, err - } - return files, nil -} +// SaveSubscriberAddress saves or updates a subscriber address in the database +func (store *GormSubscriberStore) SaveSubscriberAddress(address *types.SubscriberAddress) error { + var existingAddress types.SubscriberAddress + result := store.DB.Where("address = ?", address.Address).First(&existingAddress) -// GetRecentUploads gets the most recent uploads for a subscriber -func (store *GormSubscriberStore) GetRecentUploads(npub string, limit int) ([]*types.FileUpload, error) { - var files []*types.FileUpload - if err := store.DB.Where("npub = ? AND deleted = ?", npub, false). - Order("created_at desc"). - Limit(limit). - Find(&files).Error; err != nil { - return nil, err + if result.Error != nil && result.Error != gorm.ErrRecordNotFound { + log.Printf("Error querying existing address: %v", result.Error) + return result.Error } - return files, nil -} -// DeleteFile marks a file as deleted and updates storage usage -func (store *GormSubscriberStore) DeleteFile(npub string, fileHash string) error { - tx := store.DB.Begin() - if tx.Error != nil { - return tx.Error - } - defer func() { - if r := recover(); r != nil { - tx.Rollback() + // If the address already exists, update it + if result.RowsAffected > 0 { + if err := store.DB.Model(&existingAddress).Updates(address).Error; err != nil { + return fmt.Errorf("failed to update existing address: %v", err) } - }() - - var file types.FileUpload - if err := tx.Where("npub = ? AND file_hash = ? AND deleted = ?", npub, fileHash, false). - First(&file).Error; err != nil { - tx.Rollback() - return err - } - - now := time.Now() - if err := tx.Model(&file).Updates(map[string]interface{}{ - "deleted": true, - "deleted_at": &now, - }).Error; err != nil { - tx.Rollback() - return err + log.Printf("Updated existing address: %s", address.Address) + return nil } - if err := tx.Model(&types.GormSubscriber{}). - Where("npub = ?", npub). - UpdateColumn("storage_used_bytes", gorm.Expr("storage_used_bytes - ?", file.SizeBytes)). - Error; err != nil { - tx.Rollback() + // Create a new address record + if err := store.DB.Create(address).Error; err != nil { + log.Printf("Error saving new address: %v", err) return err } - return tx.Commit().Error -} - -func (store *GormSubscriberStore) DebugAddressDetails(address string) { - var addr types.SubscriberAddress - result := store.DB.Where("address = ?", address).First(&addr) - - if result.Error != nil { - log.Printf("Error querying address details: %v", result.Error) - return - } - - log.Printf("=== Address Details ===") - log.Printf("Address: %s", addr.Address) - log.Printf("Status: %s", addr.Status) - if addr.Npub != "" { - log.Printf("Allocated to npub: %s", addr.Npub) - } else { - log.Printf("Not allocated to any npub") - } - if addr.AllocatedAt != nil { - log.Printf("Allocated at: %v", *addr.AllocatedAt) - } - log.Printf("=====================") -} - -func (store *GormSubscriberStore) DumpAddressTable() { - var addresses []types.SubscriberAddress - result := store.DB.Find(&addresses) - - if result.Error != nil { - log.Printf("Error querying address table: %v", result.Error) - return - } - - log.Printf("=== Address Table Contents ===") - log.Printf("Found %d addresses", len(addresses)) - for _, addr := range addresses { - log.Printf("Address: %s, Status: %s, Npub: %v", - addr.Address, - addr.Status, - addr.Npub) - } - log.Printf("============================") -} - -// Add this debug select to check the address in the database directly -func (store *GormSubscriberStore) DebugAddressTableContent() { - var results []struct { - Address string - Status string - Npub *string - } - - store.DB.Raw(` - SELECT address, status, npub - FROM subscriber_addresses - WHERE status = 'allocated' - ORDER BY allocated_at DESC - LIMIT 5 - `).Scan(&results) - - log.Printf("=== Recent Allocated Addresses ===") - for _, r := range results { - log.Printf("Address: %s, Status: %s, Npub: %v", - r.Address, r.Status, r.Npub) - } - log.Printf("=================================") + log.Printf("Address %s saved successfully.", address.Address) + return nil } diff --git a/lib/subscription/subscription.go b/lib/subscription/subscription.go index a26c275..72d29de 100644 --- a/lib/subscription/subscription.go +++ b/lib/subscription/subscription.go @@ -25,15 +25,10 @@ type StorageInfo struct { UpdatedAt time.Time // Last time storage information was updated } -// SubscriptionManager handles all subscription-related operations including: -// - Subscriber management -// - NIP-88 event creation and updates -// - Storage tracking -// - Payment processing +// SubscriptionManager handles all subscription-related operations through NIP-88 events type SubscriptionManager struct { - store stores.Store // Interface to the storage layer - relayPrivateKey *btcec.PrivateKey // Relay's private key for signing events - // relayBTCAddress string // Relay's Bitcoin address for payments + store stores.Store // Interface to handle minimal state storage + relayPrivateKey *btcec.PrivateKey // Relay's private key for signing events relayDHTKey string // Relay's DHT key subscriptionTiers []lib.SubscriptionTier // Available subscription tiers } @@ -42,27 +37,23 @@ type SubscriptionManager struct { func NewSubscriptionManager( store stores.Store, relayPrivKey *btcec.PrivateKey, - // relayBTCAddress string, relayDHTKey string, tiers []lib.SubscriptionTier, ) *SubscriptionManager { return &SubscriptionManager{ - store: store, - relayPrivateKey: relayPrivKey, - // relayBTCAddress: relayBTCAddress, + store: store, + relayPrivateKey: relayPrivKey, relayDHTKey: relayDHTKey, subscriptionTiers: tiers, } } -// InitializeSubscriber creates a new subscriber or retrieves an existing one -// and creates their initial NIP-88 event. This is called when a user first -// connects to the relay. +// InitializeSubscriber creates a new subscriber or retrieves an existing one and creates their initial NIP-88 event. func (m *SubscriptionManager) InitializeSubscriber(npub string) error { - // Step 1: Get or create subscriber record - subscriber, err := m.getOrCreateSubscriber(npub) + // Step 1: Allocate a Bitcoin address (if necessary) + address, err := m.store.GetSubscriberStore().AllocateBitcoinAddress(npub) if err != nil { - return fmt.Errorf("failed to initialize subscriber: %v", err) + return fmt.Errorf("failed to allocate Bitcoin address: %v", err) } // Step 2: Create initial NIP-88 event with zero storage usage @@ -73,14 +64,13 @@ func (m *SubscriptionManager) InitializeSubscriber(npub string) error { } // Step 3: Create the NIP-88 event - return m.createOrUpdateNIP88Event(subscriber, "", time.Time{}, &storageInfo) + return m.createNIP88EventIfNotExists(&lib.Subscriber{ + Npub: npub, + Address: address.Address, + }, "", time.Time{}, &storageInfo) } -// ProcessPayment handles a new subscription payment. It: -// - Validates the payment amount against available tiers -// - Updates subscriber information -// - Creates a new subscription period -// - Updates the NIP-88 event +// ProcessPayment handles a new subscription payment by updating the NIP-88 event and other relevant data func (m *SubscriptionManager) ProcessPayment( npub string, transactionID string, @@ -92,67 +82,48 @@ func (m *SubscriptionManager) ProcessPayment( return fmt.Errorf("error matching tier: %v", err) } - // Step 2: Get existing subscriber - subscriber, err := m.store.GetSubscriber(npub) - if err != nil { - return fmt.Errorf("subscriber not found: %v", err) + // Step 2: Fetch NIP-88 event data to retrieve subscriber information + events, err := m.store.QueryEvents(nostr.Filter{ + Kinds: []int{764}, + Tags: nostr.TagMap{ + "p": []string{npub}, + }, + Limit: 1, + }) + if err != nil || len(events) == 0 { + return fmt.Errorf("no NIP-88 event found for user") } - - // Step 3: Verify transaction hasn't been processed before - existingPeriod, err := m.store.GetSubscriberStore().GetSubscriptionByTransactionID(transactionID) - if err == nil && existingPeriod != nil { - return fmt.Errorf("transaction %s already processed", transactionID) + currentEvent := events[0] + storageInfo, err := m.extractStorageInfo(currentEvent) + if err != nil { + return fmt.Errorf("failed to extract storage info: %v", err) } - // Step 4: Calculate subscription period dates - startDate := time.Now() - endDate := m.calculateEndDate(subscriber.EndDate) + // Step 3: Calculate subscription period dates and storage limit + createdAt := time.Unix(int64(currentEvent.CreatedAt), 0) + endDate := m.calculateEndDate(createdAt) storageLimit := m.calculateStorageLimit(tier.DataLimit) - // Step 5: Initialize storage tracking - storageInfo := StorageInfo{ - UsedBytes: 0, // Reset for new subscription - TotalBytes: storageLimit, - UpdatedAt: time.Now(), - } - - log.Printf("Updating NIP-88 event for subscriber %s with tier %s", npub, tier.DataLimit) - // Step 6: Update NIP-88 event - if err := m.createOrUpdateNIP88Event(subscriber, tier.DataLimit, endDate, &storageInfo); err != nil { - log.Printf("Error updating NIP-88 event: %v", err) - } - - // Step 7: Create subscription period record - period := &lib.SubscriptionPeriod{ - TransactionID: transactionID, - Tier: tier.DataLimit, - StorageLimitBytes: storageLimit, - StartDate: startDate, - EndDate: endDate, - PaymentAmount: fmt.Sprintf("%d", amountSats), - } - - if err := m.store.GetSubscriberStore().AddSubscriptionPeriod(npub, period); err != nil { - return fmt.Errorf("failed to add subscription period: %v", err) - } - - // Step 8: Update subscriber record - subscriber.Tier = tier.DataLimit - subscriber.StartDate = startDate - subscriber.EndDate = endDate - subscriber.LastTransactionID = transactionID + // Step 4: Update storage information for new subscription + storageInfo.TotalBytes = storageLimit + storageInfo.UpdatedAt = time.Now() - if err := m.store.SaveSubscriber(subscriber); err != nil { - return fmt.Errorf("failed to update subscriber: %v", err) + // Step 5: Update NIP-88 event with new subscription details + address := getTagValue(currentEvent.Tags, "relay_bitcoin_address") + if err := m.createOrUpdateNIP88Event(&lib.Subscriber{ + Npub: npub, + Address: address, + }, tier.DataLimit, endDate, &storageInfo); err != nil { + return fmt.Errorf("error updating NIP-88 event: %v", err) } + log.Printf("Processed payment for subscriber %s with tier %s", npub, tier.DataLimit) return nil } -// UpdateStorageUsage updates the storage usage for a subscriber when they upload or delete files -// It updates both the subscriber store and the NIP-88 event +// UpdateStorageUsage updates the storage usage for a subscriber by modifying the relevant NIP-88 event func (m *SubscriptionManager) UpdateStorageUsage(npub string, newBytes int64) error { - // Step 1: Get current NIP-88 event to check current storage usage + // Fetch current NIP-88 event data events, err := m.store.QueryEvents(nostr.Filter{ Kinds: []int{764}, Tags: nostr.TagMap{ @@ -163,87 +134,74 @@ func (m *SubscriptionManager) UpdateStorageUsage(npub string, newBytes int64) er if err != nil || len(events) == 0 { return fmt.Errorf("no NIP-88 event found for user") } - currentEvent := events[0] - // Step 2: Get current storage information + // Extract and update storage information storageInfo, err := m.extractStorageInfo(currentEvent) if err != nil { return fmt.Errorf("failed to extract storage info: %v", err) } - - // Step 3: Validate new storage usage newUsedBytes := storageInfo.UsedBytes + newBytes if newUsedBytes > storageInfo.TotalBytes { - return fmt.Errorf("storage limit exceeded: would use %d of %d bytes", - newUsedBytes, storageInfo.TotalBytes) + return fmt.Errorf("storage limit exceeded: would use %d of %d bytes", newUsedBytes, storageInfo.TotalBytes) } - - // Step 4: Update storage tracking storageInfo.UsedBytes = newUsedBytes storageInfo.UpdatedAt = time.Now() - // Step 5: Get subscriber record - subscriber, err := m.store.GetSubscriber(npub) - if err != nil { - return fmt.Errorf("failed to get subscriber: %v", err) - } - - // Step 6: Update storage usage in subscriber store - if err := m.store.GetSubscriberStore().UpdateStorageUsage(npub, newBytes); err != nil { - return fmt.Errorf("failed to update storage usage: %v", err) - } - - // Step 7: Get current subscription info from event tags - activeTier := "" - var expirationDate time.Time - for _, tag := range currentEvent.Tags { - if tag[0] == "active_subscription" && len(tag) >= 3 { - activeTier = tag[1] - timestamp, _ := strconv.ParseInt(tag[2], 10, 64) - expirationDate = time.Unix(timestamp, 0) - break - } - } + // Replacing `GetValue` and `GetUnixValue` calls with utility functions + activeSubscription := getTagValue(currentEvent.Tags, "active_subscription") + expirationTime := time.Unix(getTagUnixValue(currentEvent.Tags, "active_subscription"), 0) - // Step 8: Update NIP-88 event with new storage information - return m.createOrUpdateNIP88Event(subscriber, activeTier, expirationDate, &storageInfo) + // Update NIP-88 event + return m.createOrUpdateNIP88Event(&lib.Subscriber{ + Npub: npub, + }, activeSubscription, expirationTime, &storageInfo) } -// Private helper methods - -// getOrCreateSubscriber retrieves an existing subscriber or creates a new one -func (m *SubscriptionManager) getOrCreateSubscriber(npub string) (*lib.Subscriber, error) { - subscriber, err := m.store.GetSubscriber(npub) - if err == nil { - return subscriber, nil +// CheckStorageAvailability checks if a subscriber has enough available storage for a given number of bytes. +// It retrieves storage data from the user's NIP-88 event and validates against their current usage and limits. +func (m *SubscriptionManager) CheckStorageAvailability(npub string, requestedBytes int64) error { + // Step 1: Fetch the user's NIP-88 event + events, err := m.store.QueryEvents(nostr.Filter{ + Kinds: []int{764}, + Tags: nostr.TagMap{ + "p": []string{npub}, + }, + Limit: 1, + }) + if err != nil || len(events) == 0 { + return fmt.Errorf("no NIP-88 event found for user: %s", npub) } + currentEvent := events[0] - // Allocate a unique Bitcoin address for the new subscriber - address, err := m.store.GetSubscriberStore().AllocateBitcoinAddress(npub) + // Step 2: Extract storage information from the event + storageInfo, err := m.extractStorageInfo(currentEvent) if err != nil { - return nil, fmt.Errorf("failed to allocate Bitcoin address: %v", err) + return fmt.Errorf("failed to extract storage info: %v", err) } - log.Println("User allocated address: ", address.Address) - - testAddress := "bc1qfjqax7sm9s5zcxwyq4r2shqlywh9re2l35mxa4" - - // Create new subscriber with default values - newSubscriber := &lib.Subscriber{ - Npub: npub, - Tier: "", - StartDate: time.Time{}, - EndDate: time.Time{}, - Address: testAddress, - LastTransactionID: "", + // Step 3: Check if the user has enough available storage + newUsage := storageInfo.UsedBytes + requestedBytes + if newUsage > storageInfo.TotalBytes { + return fmt.Errorf("storage limit exceeded: would use %d of %d bytes", newUsage, storageInfo.TotalBytes) } - if err := m.store.SaveSubscriber(newSubscriber); err != nil { - return nil, err + // Step 4: Optionally, check if the subscription is still active + for _, tag := range currentEvent.Tags { + if tag[0] == "active_subscription" && len(tag) >= 3 { + expirationTimestamp, err := strconv.ParseInt(tag[2], 10, 64) + if err != nil { + return fmt.Errorf("invalid subscription expiration timestamp: %v", err) + } + expirationDate := time.Unix(expirationTimestamp, 0) + if time.Now().After(expirationDate) { + return fmt.Errorf("subscription has expired") + } + break + } } - return newSubscriber, nil + return nil } // createOrUpdateNIP88Event creates or updates a subscriber's NIP-88 event @@ -253,7 +211,7 @@ func (m *SubscriptionManager) createOrUpdateNIP88Event( expirationDate time.Time, storageInfo *StorageInfo, ) error { - // Step 1: Delete existing NIP-88 event if it exists + // Delete existing NIP-88 event if it exists existingEvents, err := m.store.QueryEvents(nostr.Filter{ Kinds: []int{764}, Tags: nostr.TagMap{ @@ -267,36 +225,84 @@ func (m *SubscriptionManager) createOrUpdateNIP88Event( } } - // Step 2: Prepare event tags + // Prepare tags and create a new NIP-88 event tags := []nostr.Tag{ {"subscription_duration", "1 month"}, {"p", subscriber.Npub}, {"subscription_status", m.getSubscriptionStatus(activeTier)}, {"relay_bitcoin_address", subscriber.Address}, {"relay_dht_key", m.relayDHTKey}, - // Add storage information tag - {"storage", - fmt.Sprintf("%d", storageInfo.UsedBytes), - fmt.Sprintf("%d", storageInfo.TotalBytes), - fmt.Sprintf("%d", storageInfo.UpdatedAt.Unix()), + {"storage", fmt.Sprintf("%d", storageInfo.UsedBytes), fmt.Sprintf("%d", storageInfo.TotalBytes), fmt.Sprintf("%d", storageInfo.UpdatedAt.Unix())}, + } + + if activeTier != "" { + tags = append(tags, nostr.Tag{ + "active_subscription", activeTier, fmt.Sprintf("%d", expirationDate.Unix()), + }) + } + + event := &nostr.Event{ + PubKey: hex.EncodeToString(m.relayPrivateKey.PubKey().SerializeCompressed()), + CreatedAt: nostr.Timestamp(time.Now().Unix()), + Kind: 764, + Tags: tags, + Content: "", + } + + // Sign and store the event + serializedEvent := event.Serialize() + hash := sha256.Sum256(serializedEvent) + event.ID = hex.EncodeToString(hash[:]) + sig, err := schnorr.Sign(m.relayPrivateKey, hash[:]) + if err != nil { + return fmt.Errorf("error signing event: %v", err) + } + event.Sig = hex.EncodeToString(sig.Serialize()) + + return m.store.StoreEvent(event) +} + +// createNIP88EventIfNotExists creates a new NIP-88 event for a subscriber if none exists +func (m *SubscriptionManager) createNIP88EventIfNotExists( + subscriber *lib.Subscriber, + activeTier string, + expirationDate time.Time, + storageInfo *StorageInfo, +) error { + // Check if an existing NIP-88 event for the subscriber already exists + existingEvents, err := m.store.QueryEvents(nostr.Filter{ + Kinds: []int{764}, // Assuming 764 is the NIP-88 event kind + Tags: nostr.TagMap{ + "p": []string{subscriber.Npub}, }, + Limit: 1, + }) + if err != nil { + return fmt.Errorf("error querying existing NIP-88 events: %v", err) } - // Add available subscription tiers - for _, tier := range m.subscriptionTiers { - tags = append(tags, nostr.Tag{"subscription-tier", tier.DataLimit, tier.Price}) + // If an existing event is found, we skip creation + if len(existingEvents) > 0 { + log.Printf("NIP-88 event already exists for subscriber %s, skipping creation", subscriber.Npub) + return nil + } + + // Prepare tags for the new NIP-88 event + tags := []nostr.Tag{ + {"subscription_duration", "1 month"}, + {"p", subscriber.Npub}, + {"subscription_status", m.getSubscriptionStatus(activeTier)}, + {"relay_bitcoin_address", subscriber.Address}, + {"relay_dht_key", m.relayDHTKey}, + {"storage", fmt.Sprintf("%d", storageInfo.UsedBytes), fmt.Sprintf("%d", storageInfo.TotalBytes), fmt.Sprintf("%d", storageInfo.UpdatedAt.Unix())}, } - // Add active subscription info if applicable if activeTier != "" { tags = append(tags, nostr.Tag{ - "active_subscription", - activeTier, - fmt.Sprintf("%d", expirationDate.Unix()), + "active_subscription", activeTier, fmt.Sprintf("%d", expirationDate.Unix()), }) } - // Step 3: Create new event event := &nostr.Event{ PubKey: hex.EncodeToString(m.relayPrivateKey.PubKey().SerializeCompressed()), CreatedAt: nostr.Timestamp(time.Now().Unix()), @@ -305,24 +311,19 @@ func (m *SubscriptionManager) createOrUpdateNIP88Event( Content: "", } - // Generate event ID + // Sign and store the new event serializedEvent := event.Serialize() hash := sha256.Sum256(serializedEvent) event.ID = hex.EncodeToString(hash[:]) - - // Sign event sig, err := schnorr.Sign(m.relayPrivateKey, hash[:]) if err != nil { return fmt.Errorf("error signing event: %v", err) } event.Sig = hex.EncodeToString(sig.Serialize()) - // Step 4: Store the event return m.store.StoreEvent(event) } -// Helper functions - // findMatchingTier finds the highest tier that matches the payment amount func (m *SubscriptionManager) findMatchingTier(amountSats int64) (*lib.SubscriptionTier, error) { var bestMatch *lib.SubscriptionTier @@ -344,36 +345,6 @@ func (m *SubscriptionManager) findMatchingTier(amountSats int64) (*lib.Subscript return bestMatch, nil } -// calculateEndDate determines the subscription end date -func (m *SubscriptionManager) calculateEndDate(currentEnd time.Time) time.Time { - if time.Now().Before(currentEnd) { - return currentEnd.AddDate(0, 1, 0) // Extend by 1 month - } - return time.Now().AddDate(0, 1, 0) // Start new 1 month period -} - -// calculateStorageLimit converts tier string to bytes -func (m *SubscriptionManager) calculateStorageLimit(tier string) int64 { - switch tier { - case "1 GB per month": - return 1 * 1024 * 1024 * 1024 - case "5 GB per month": - return 5 * 1024 * 1024 * 1024 - case "10 GB per month": - return 10 * 1024 * 1024 * 1024 - default: - return 0 - } -} - -// getSubscriptionStatus returns the subscription status string -func (m *SubscriptionManager) getSubscriptionStatus(activeTier string) string { - if activeTier == "" { - return "inactive" - } - return "active" -} - // parseSats converts price string to satoshis func (m *SubscriptionManager) parseSats(price string) int64 { var sats int64 @@ -417,421 +388,53 @@ func (m *SubscriptionManager) extractStorageInfo(event *nostr.Event) (StorageInf }, nil } -// package subscription - -// import ( -// "crypto/sha256" -// "encoding/hex" -// "fmt" -// "log" -// "strconv" -// "time" - -// "github.com/btcsuite/btcd/btcec/v2" -// "github.com/btcsuite/btcd/btcec/v2/schnorr" -// "github.com/nbd-wtf/go-nostr" - -// "github.com/HORNET-Storage/hornet-storage/lib" -// "github.com/HORNET-Storage/hornet-storage/lib/stores" -// ) - -// // StorageInfo tracks current storage usage information for a subscriber -// type StorageInfo struct { -// UsedBytes int64 // Current bytes used by the subscriber -// TotalBytes int64 // Total bytes allocated to the subscriber -// UpdatedAt time.Time // Last time storage information was updated -// } - -// // SubscriptionManager handles all subscription-related operations including: -// // - Subscriber management -// // - NIP-88 event creation and updates -// // - Storage tracking -// // - Payment processing -// type SubscriptionManager struct { -// store stores.Store // Interface to the storage layer -// relayPrivateKey *btcec.PrivateKey // Relay's private key for signing events -// // relayBTCAddress string // Relay's Bitcoin address for payments -// relayDHTKey string // Relay's DHT key -// subscriptionTiers []lib.SubscriptionTier // Available subscription tiers -// } - -// // NewSubscriptionManager creates and initializes a new subscription manager -// func NewSubscriptionManager( -// store stores.Store, -// relayPrivKey *btcec.PrivateKey, -// // relayBTCAddress string, -// relayDHTKey string, -// tiers []lib.SubscriptionTier, -// ) *SubscriptionManager { -// return &SubscriptionManager{ -// store: store, -// relayPrivateKey: relayPrivKey, -// // relayBTCAddress: relayBTCAddress, -// relayDHTKey: relayDHTKey, -// subscriptionTiers: tiers, -// } -// } - -// // InitializeSubscriber creates a new subscriber or retrieves an existing one -// // and creates their initial NIP-88 event. This is called when a user first -// // connects to the relay. -// func (m *SubscriptionManager) InitializeSubscriber(npub string) error { -// // Step 1: Get or create subscriber record -// subscriber, err := m.getOrCreateSubscriber(npub) -// if err != nil { -// return fmt.Errorf("failed to initialize subscriber: %v", err) -// } - -// // Step 2: Create initial NIP-88 event with zero storage usage -// storageInfo := StorageInfo{ -// UsedBytes: 0, -// TotalBytes: 0, -// UpdatedAt: time.Now(), -// } - -// // Step 3: Create the NIP-88 event -// return m.createOrUpdateNIP88Event(subscriber, "", time.Time{}, &storageInfo) -// } - -// // ProcessPayment handles a new subscription payment. It: -// // - Validates the payment amount against available tiers -// // - Updates subscriber information -// // - Creates a new subscription period -// // - Updates the NIP-88 event -// // ProcessPayment handles a new subscription payment and updates the NIP-88 event -// func (m *SubscriptionManager) ProcessPayment(npub string, transactionID string, amountSats int64) error { -// log.Printf("Starting ProcessPayment for npub: %s, transactionID: %s, amountSats: %d", npub, transactionID, amountSats) - -// // Step 1: Match payment amount to a subscription tier -// tier, err := m.findMatchingTier(amountSats) -// if err != nil { -// log.Printf("Error matching tier for amount %d: %v", amountSats, err) -// return fmt.Errorf("error matching tier: %v", err) -// } -// log.Printf("Matched tier: %s for payment of %d sats", tier.DataLimit, amountSats) - -// // Step 2: Retrieve the existing subscriber -// subscriber, err := m.store.GetSubscriber(npub) -// if err != nil { -// log.Printf("Error retrieving subscriber with npub %s: %v", npub, err) -// return fmt.Errorf("subscriber not found: %v", err) -// } -// log.Printf("Retrieved subscriber: %v", subscriber) - -// // Step 3: Check if this transaction has already been processed -// existingPeriod, err := m.store.GetSubscriberStore().GetSubscriptionByTransactionID(transactionID) -// if err == nil && existingPeriod != nil { -// log.Printf("Transaction %s has already been processed for subscriber %s", transactionID, npub) -// return fmt.Errorf("transaction %s already processed", transactionID) -// } - -// // Step 4: Calculate subscription period dates and storage limit -// startDate := time.Now() -// endDate := m.calculateEndDate(subscriber.EndDate) -// storageLimit := m.calculateStorageLimit(tier.DataLimit) -// log.Printf("Calculated subscription period: StartDate=%v, EndDate=%v, StorageLimit=%d bytes", startDate, endDate, storageLimit) - -// // Step 5: Initialize storage tracking for new subscription period -// storageInfo := StorageInfo{ -// UsedBytes: 0, -// TotalBytes: storageLimit, -// UpdatedAt: time.Now(), -// } -// log.Printf("Initialized StorageInfo: %v", storageInfo) - -// // Step 6: Create a new subscription period record -// period := &lib.SubscriptionPeriod{ -// TransactionID: transactionID, -// Tier: tier.DataLimit, -// StorageLimitBytes: storageLimit, -// StartDate: startDate, -// EndDate: endDate, -// PaymentAmount: fmt.Sprintf("%d", amountSats), -// } -// if err := m.store.GetSubscriberStore().AddSubscriptionPeriod(npub, period); err != nil { -// log.Printf("Error adding subscription period for subscriber %s: %v", npub, err) -// return fmt.Errorf("failed to add subscription period: %v", err) -// } -// log.Printf("Added subscription period: %v", period) - -// // Step 7: Update the subscriber record with the new subscription information -// subscriber.Tier = tier.DataLimit -// subscriber.StartDate = startDate -// subscriber.EndDate = endDate -// subscriber.LastTransactionID = transactionID -// if err := m.store.SaveSubscriber(subscriber); err != nil { -// log.Printf("Error saving updated subscriber %s: %v", npub, err) -// return fmt.Errorf("failed to update subscriber: %v", err) -// } -// log.Printf("Updated subscriber record: %v", subscriber) - -// // Step 8: Update the NIP-88 event for the subscriber -// if err := m.createOrUpdateNIP88Event(subscriber, tier.DataLimit, endDate, &storageInfo); err != nil { -// log.Printf("Error updating NIP-88 event for subscriber %s: %v", npub, err) -// return fmt.Errorf("failed to update NIP-88 event: %v", err) -// } -// log.Printf("Successfully updated NIP-88 event for subscriber %s with tier %s", npub, tier.DataLimit) - -// log.Printf("ProcessPayment completed successfully for npub: %s, transactionID: %s", npub, transactionID) -// return nil -// } - -// // UpdateStorageUsage updates the storage usage for a subscriber when they upload or delete files -// // It updates both the subscriber store and the NIP-88 event -// func (m *SubscriptionManager) UpdateStorageUsage(npub string, newBytes int64) error { -// // Step 1: Get current NIP-88 event to check current storage usage -// events, err := m.store.QueryEvents(nostr.Filter{ -// Kinds: []int{764}, -// Tags: nostr.TagMap{ -// "p": []string{npub}, -// }, -// Limit: 1, -// }) -// if err != nil || len(events) == 0 { -// return fmt.Errorf("no NIP-88 event found for user") -// } - -// currentEvent := events[0] - -// // Step 2: Get current storage information -// storageInfo, err := m.extractStorageInfo(currentEvent) -// if err != nil { -// return fmt.Errorf("failed to extract storage info: %v", err) -// } - -// // Step 3: Validate new storage usage -// newUsedBytes := storageInfo.UsedBytes + newBytes -// if newUsedBytes > storageInfo.TotalBytes { -// return fmt.Errorf("storage limit exceeded: would use %d of %d bytes", -// newUsedBytes, storageInfo.TotalBytes) -// } - -// // Step 4: Update storage tracking -// storageInfo.UsedBytes = newUsedBytes -// storageInfo.UpdatedAt = time.Now() - -// // Step 5: Get subscriber record -// subscriber, err := m.store.GetSubscriber(npub) -// if err != nil { -// return fmt.Errorf("failed to get subscriber: %v", err) -// } - -// // Step 6: Update storage usage in subscriber store -// if err := m.store.GetSubscriberStore().UpdateStorageUsage(npub, newBytes); err != nil { -// return fmt.Errorf("failed to update storage usage: %v", err) -// } - -// // Step 7: Get current subscription info from event tags -// activeTier := "" -// var expirationDate time.Time -// for _, tag := range currentEvent.Tags { -// if tag[0] == "active_subscription" && len(tag) >= 3 { -// activeTier = tag[1] -// timestamp, _ := strconv.ParseInt(tag[2], 10, 64) -// expirationDate = time.Unix(timestamp, 0) -// break -// } -// } - -// // Step 8: Update NIP-88 event with new storage information -// return m.createOrUpdateNIP88Event(subscriber, activeTier, expirationDate, &storageInfo) -// } - -// // Private helper methods - -// // getOrCreateSubscriber retrieves an existing subscriber or creates a new one -// func (m *SubscriptionManager) getOrCreateSubscriber(npub string) (*lib.Subscriber, error) { -// subscriber, err := m.store.GetSubscriber(npub) -// if err == nil { -// return subscriber, nil -// } - -// // Allocate a unique Bitcoin address for the new subscriber -// address, err := m.store.GetSubscriberStore().AllocateBitcoinAddress(npub) -// if err != nil { -// return nil, fmt.Errorf("failed to allocate Bitcoin address: %v", err) -// } - -// // Create new subscriber with allocated address -// newSubscriber := &lib.Subscriber{ -// Npub: npub, -// Tier: "", -// StartDate: time.Time{}, -// EndDate: time.Time{}, -// Address: address.Address, // Using allocated address -// LastTransactionID: "", -// } - -// if err := m.store.SaveSubscriber(newSubscriber); err != nil { -// return nil, err -// } - -// log.Printf("Created new subscriber %s with allocated address %s", npub, address.Address) -// return newSubscriber, nil -// } - -// // createOrUpdateNIP88Event creates or updates a subscriber's NIP-88 event -// func (m *SubscriptionManager) createOrUpdateNIP88Event( -// subscriber *lib.Subscriber, -// activeTier string, -// expirationDate time.Time, -// storageInfo *StorageInfo, -// ) error { -// // Step 1: Delete existing NIP-88 event if it exists -// existingEvents, err := m.store.QueryEvents(nostr.Filter{ -// Kinds: []int{764}, -// Tags: nostr.TagMap{ -// "p": []string{subscriber.Npub}, -// }, -// Limit: 1, -// }) -// if err == nil && len(existingEvents) > 0 { -// if err := m.store.DeleteEvent(existingEvents[0].ID); err != nil { -// log.Printf("Warning: failed to delete existing NIP-88 event: %v", err) -// } -// } - -// // Step 2: Prepare event tags with "paid" or "active" status -// subscriptionStatus := "inactive" -// if activeTier != "" { -// subscriptionStatus = "active" -// } -// tags := []nostr.Tag{ -// {"subscription_duration", "1 month"}, -// {"p", subscriber.Npub}, -// {"subscription_status", subscriptionStatus}, -// {"relay_bitcoin_address", subscriber.Address}, -// {"relay_dht_key", m.relayDHTKey}, -// {"storage", fmt.Sprintf("%d", storageInfo.UsedBytes), -// fmt.Sprintf("%d", storageInfo.TotalBytes), -// fmt.Sprintf("%d", storageInfo.UpdatedAt.Unix())}, -// } -// if activeTier != "" { -// tags = append(tags, nostr.Tag{ -// "active_subscription", activeTier, fmt.Sprintf("%d", expirationDate.Unix()), -// }) -// } - -// // Additional subscription tiers -// for _, tier := range m.subscriptionTiers { -// tags = append(tags, nostr.Tag{"subscription-tier", tier.DataLimit, tier.Price}) -// } - -// // Step 3: Create and sign the new event -// event := &nostr.Event{ -// PubKey: hex.EncodeToString(m.relayPrivateKey.PubKey().SerializeCompressed()), -// CreatedAt: nostr.Timestamp(time.Now().Unix()), -// Kind: 764, -// Tags: tags, -// Content: "", -// } -// serializedEvent := event.Serialize() -// hash := sha256.Sum256(serializedEvent) -// event.ID = hex.EncodeToString(hash[:]) - -// sig, err := schnorr.Sign(m.relayPrivateKey, hash[:]) -// if err != nil { -// return fmt.Errorf("error signing event: %v", err) -// } -// event.Sig = hex.EncodeToString(sig.Serialize()) - -// // Step 4: Store the event and log status -// log.Printf("Storing NIP-88 event with ID: %s and subscription status: %s", event.ID, subscriptionStatus) -// return m.store.StoreEvent(event) -// } - -// // Helper functions - -// // findMatchingTier finds the highest tier that matches the payment amount -// func (m *SubscriptionManager) findMatchingTier(amountSats int64) (*lib.SubscriptionTier, error) { -// var bestMatch *lib.SubscriptionTier -// var bestPrice int64 - -// for _, tier := range m.subscriptionTiers { -// price := m.parseSats(tier.Price) -// if amountSats >= price && price > bestPrice { -// tierCopy := tier -// bestMatch = &tierCopy -// bestPrice = price -// } -// } - -// if bestMatch == nil { -// return nil, fmt.Errorf("no matching tier for payment of %d sats", amountSats) -// } - -// return bestMatch, nil -// } - -// // calculateEndDate determines the subscription end date -// func (m *SubscriptionManager) calculateEndDate(currentEnd time.Time) time.Time { -// if time.Now().Before(currentEnd) { -// return currentEnd.AddDate(0, 1, 0) // Extend by 1 month -// } -// return time.Now().AddDate(0, 1, 0) // Start new 1 month period -// } - -// // calculateStorageLimit converts tier string to bytes -// func (m *SubscriptionManager) calculateStorageLimit(tier string) int64 { -// switch tier { -// case "1 GB per month": -// return 1 * 1024 * 1024 * 1024 -// case "5 GB per month": -// return 5 * 1024 * 1024 * 1024 -// case "10 GB per month": -// return 10 * 1024 * 1024 * 1024 -// default: -// return 0 -// } -// } - -// // getSubscriptionStatus returns the subscription status string -// // func (m *SubscriptionManager) getSubscriptionStatus(activeTier string) string { -// // if activeTier == "" { -// // return "inactive" -// // } -// // return "active" -// // } - -// // parseSats converts price string to satoshis -// func (m *SubscriptionManager) parseSats(price string) int64 { -// var sats int64 -// fmt.Sscanf(price, "%d", &sats) -// return sats -// } - -// // extractStorageInfo gets storage information from NIP-88 event -// func (m *SubscriptionManager) extractStorageInfo(event *nostr.Event) (StorageInfo, error) { -// var info StorageInfo - -// for _, tag := range event.Tags { -// if tag[0] == "storage" && len(tag) >= 4 { -// used, err := strconv.ParseInt(tag[1], 10, 64) -// if err != nil { -// return info, fmt.Errorf("invalid used storage value: %v", err) -// } - -// total, err := strconv.ParseInt(tag[2], 10, 64) -// if err != nil { -// return info, fmt.Errorf("invalid total storage value: %v", err) -// } - -// updated, err := strconv.ParseInt(tag[3], 10, 64) -// if err != nil { -// return info, fmt.Errorf("invalid update timestamp: %v", err) -// } - -// info.UsedBytes = used -// info.TotalBytes = total -// info.UpdatedAt = time.Unix(updated, 0) -// return info, nil -// } -// } - -// // Return zero values if no storage tag found -// return StorageInfo{ -// UsedBytes: 0, -// TotalBytes: 0, -// UpdatedAt: time.Now(), -// }, nil -// } +// getSubscriptionStatus returns the subscription status string +func (m *SubscriptionManager) getSubscriptionStatus(activeTier string) string { + if activeTier == "" { + return "inactive" + } + return "active" +} + +// calculateStorageLimit converts tier string to bytes +func (m *SubscriptionManager) calculateStorageLimit(tier string) int64 { + switch tier { + case "1 GB per month": + return 1 * 1024 * 1024 * 1024 + case "5 GB per month": + return 5 * 1024 * 1024 * 1024 + case "10 GB per month": + return 10 * 1024 * 1024 * 1024 + default: + return 0 + } +} + +// calculateEndDate determines the subscription end date +func (m *SubscriptionManager) calculateEndDate(currentEnd time.Time) time.Time { + if time.Now().Before(currentEnd) { + return currentEnd.AddDate(0, 1, 0) // Extend by 1 month + } + return time.Now().AddDate(0, 1, 0) // Start new 1 month period +} + +func getTagValue(tags []nostr.Tag, key string) string { + for _, tag := range tags { + if len(tag) > 1 && tag[0] == key { + return tag[1] + } + } + return "" +} + +func getTagUnixValue(tags []nostr.Tag, key string) int64 { + for _, tag := range tags { + if len(tag) > 2 && tag[0] == key { + unixTime, err := strconv.ParseInt(tag[2], 10, 64) + if err == nil { + return unixTime + } + } + } + return 0 +} diff --git a/lib/types.go b/lib/types.go index 1dd616b..9b913af 100644 --- a/lib/types.go +++ b/lib/types.go @@ -262,7 +262,7 @@ type Address struct { Npub string `json:"npub,omitempty"` } -// subscriptionAddress represents the GORM-compatible model for storing addresses +// SubscriberAddress represents the GORM-compatible model for storing addresses type SubscriberAddress struct { ID uint `gorm:"primaryKey"` Index string `gorm:"not null"` @@ -270,7 +270,7 @@ type SubscriberAddress struct { WalletName string `gorm:"not null"` Status string `gorm:"default:'available'"` AllocatedAt *time.Time `gorm:"default:null"` - Npub string `gorm:"type:text"` // Change to pointer to properly handle NULL + Npub *string `gorm:"type:text;unique"` // Pointer type and unique constraint } // type User struct { @@ -502,3 +502,10 @@ func (gs *GormSubscriber) ToSubscriber() *Subscriber { EndDate: gs.EndDate, } } + +// StorageInfo tracks current storage usage information for a subscriber +type StorageInfo struct { + UsedBytes int64 // Current bytes used by the subscriber + TotalBytes int64 // Total bytes allocated to the subscriber + UpdatedAt time.Time // Last time storage information was updated +} diff --git a/lib/web/handler_wallet_addresses.go b/lib/web/handler_wallet_addresses.go index f3960d8..98708c4 100644 --- a/lib/web/handler_wallet_addresses.go +++ b/lib/web/handler_wallet_addresses.go @@ -94,17 +94,6 @@ func saveWalletAddresses(c *fiber.Ctx, store stores.Store) error { // Save WalletAddress to SubscriberStore if it doesn't exist if !existsInSubscriberStore { - newSubscriberAddress := types.WalletAddress{ - Index: addr.Index, - Address: addr.Address, - } - log.Printf("Attempting to save new WalletAddress to SubscriberStore: %v", newSubscriberAddress) - if err := store.GetSubscriberStore().SaveSubscriberAddresses(&newSubscriberAddress); err != nil { - log.Printf("Error saving WalletAddress to SubscriberStore: %v", err) - continue - } - log.Printf("WalletAddress saved to SubscriberStore: %v", newSubscriberAddress) - // Save Subscriber-specific data to SubscriberStore subscriptionAddress := &types.SubscriberAddress{ Index: fmt.Sprint(addr.Index), @@ -112,7 +101,7 @@ func saveWalletAddresses(c *fiber.Ctx, store stores.Store) error { WalletName: addr.WalletName, Status: AddressStatusAvailable, AllocatedAt: &time.Time{}, - Npub: "", // Use nil for empty pointer + Npub: nil, // Use nil for empty pointer } log.Printf("Attempting to save SubscriberAddress to SubscriberStore: %v", subscriptionAddress) if err := store.GetSubscriberStore().SaveSubscriberAddress(subscriptionAddress); err != nil { From a16d66a6c821d34d48e7f450f332ba6de4197d97 Mon Sep 17 00:00:00 2001 From: Maphikza Date: Sat, 9 Nov 2024 14:20:59 +0200 Subject: [PATCH 12/21] Adding auto address generation implentation --- lib/stores/subscriber_store.go | 1 + .../subscription_store_gorm.go | 33 +- lib/subscription/subscription.go | 109 +++++++ lib/web/handler_wallet_addresses.go | 292 +++++++++++++----- 4 files changed, 342 insertions(+), 93 deletions(-) diff --git a/lib/stores/subscriber_store.go b/lib/stores/subscriber_store.go index 8a2b34d..82d3a55 100644 --- a/lib/stores/subscriber_store.go +++ b/lib/stores/subscriber_store.go @@ -13,4 +13,5 @@ type SubscriberStore interface { AllocateBitcoinAddress(npub string) (*lib.Address, error) AddressExists(address string) (bool, error) SaveSubscriberAddress(address *lib.SubscriberAddress) error + CountAvailableAddresses() (int64, error) } diff --git a/lib/stores/subscription_store/subscription_store_gorm.go b/lib/stores/subscription_store/subscription_store_gorm.go index 7ac2193..bfe8d3b 100644 --- a/lib/stores/subscription_store/subscription_store_gorm.go +++ b/lib/stores/subscription_store/subscription_store_gorm.go @@ -129,24 +129,7 @@ func (store *GormSubscriberStore) AddressExists(address string) (bool, error) { // SaveSubscriberAddress saves or updates a subscriber address in the database func (store *GormSubscriberStore) SaveSubscriberAddress(address *types.SubscriberAddress) error { - var existingAddress types.SubscriberAddress - result := store.DB.Where("address = ?", address.Address).First(&existingAddress) - - if result.Error != nil && result.Error != gorm.ErrRecordNotFound { - log.Printf("Error querying existing address: %v", result.Error) - return result.Error - } - - // If the address already exists, update it - if result.RowsAffected > 0 { - if err := store.DB.Model(&existingAddress).Updates(address).Error; err != nil { - return fmt.Errorf("failed to update existing address: %v", err) - } - log.Printf("Updated existing address: %s", address.Address) - return nil - } - - // Create a new address record + // Directly create a new address record if err := store.DB.Create(address).Error; err != nil { log.Printf("Error saving new address: %v", err) return err @@ -155,3 +138,17 @@ func (store *GormSubscriberStore) SaveSubscriberAddress(address *types.Subscribe log.Printf("Address %s saved successfully.", address.Address) return nil } + +// CountAvailableAddresses counts the number of available addresses in the database +func (store *GormSubscriberStore) CountAvailableAddresses() (int64, error) { + var count int64 + err := store.DB.Model(&types.SubscriberAddress{}). + Where("status = ?", AddressStatusAvailable). + Count(&count).Error + + if err != nil { + return 0, fmt.Errorf("failed to count available addresses: %v", err) + } + + return count, nil +} diff --git a/lib/subscription/subscription.go b/lib/subscription/subscription.go index 72d29de..79996f8 100644 --- a/lib/subscription/subscription.go +++ b/lib/subscription/subscription.go @@ -3,21 +3,34 @@ package subscription import ( + "bytes" + "crypto/hmac" "crypto/sha256" "encoding/hex" + "encoding/json" "fmt" "log" + "net/http" "strconv" "time" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/golang-jwt/jwt/v4" "github.com/nbd-wtf/go-nostr" + "github.com/spf13/viper" "github.com/HORNET-Storage/hornet-storage/lib" "github.com/HORNET-Storage/hornet-storage/lib/stores" ) +// Address status constants +const ( + AddressStatusAvailable = "available" + AddressStatusAllocated = "allocated" + AddressStatusUsed = "used" +) + // StorageInfo tracks current storage usage information for a subscriber type StorageInfo struct { UsedBytes int64 // Current bytes used by the subscriber @@ -50,6 +63,14 @@ func NewSubscriptionManager( // InitializeSubscriber creates a new subscriber or retrieves an existing one and creates their initial NIP-88 event. func (m *SubscriptionManager) InitializeSubscriber(npub string) error { + + // Run address pool check in background + go func() { + if err := m.checkAddressPoolStatus(); err != nil { + log.Printf("Warning: error checking address pool status: %v", err) + } + }() + // Step 1: Allocate a Bitcoin address (if necessary) address, err := m.store.GetSubscriberStore().AllocateBitcoinAddress(npub) if err != nil { @@ -388,6 +409,94 @@ func (m *SubscriptionManager) extractStorageInfo(event *nostr.Event) (StorageInf }, nil } +// checkAddressPoolStatus checks if we need to generate more addresses +func (m *SubscriptionManager) checkAddressPoolStatus() error { + availableCount, err := m.store.GetSubscriberStore().CountAvailableAddresses() + if err != nil { + return fmt.Errorf("failed to count available addresses: %v", err) + } + + log.Println("Available count: ", availableCount) + + // If we have less than 50% of addresses available, request more + if availableCount < 50 { + log.Printf("Address pool running low (%d available). Requesting 100 new addresses", availableCount) + return m.requestNewAddresses(20) + } + + return nil +} + +// requestNewAddresses sends a request to the wallet to generate new addresses +func (m *SubscriptionManager) requestNewAddresses(count int) error { + // Get API key from config + apiKey := viper.GetString("wallet_api_key") + + // Generate JWT token using API key + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "api_key": apiKey, + "exp": time.Now().Add(time.Hour * 24).Unix(), + "iat": time.Now().Unix(), + }) + + // Sign token with API key + tokenString, err := token.SignedString([]byte(apiKey)) + if err != nil { + return fmt.Errorf("failed to generate token: %v", err) + } + + reqBody := map[string]interface{}{ + "count": count, + } + + jsonData, err := json.Marshal(reqBody) + if err != nil { + return fmt.Errorf("failed to marshal request: %v", err) + } + + // Prepare HMAC signature + timestamp := time.Now().UTC().Format(time.RFC3339) + message := apiKey + timestamp + string(jsonData) + h := hmac.New(sha256.New, []byte(apiKey)) + h.Write([]byte(message)) + signature := hex.EncodeToString(h.Sum(nil)) + + // Create request + req, err := http.NewRequest("POST", + "http://localhost:9003/generate-addresses", + bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("failed to create request: %v", err) + } + + // Add all required headers including the new JWT + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", tokenString)) + req.Header.Set("X-API-Key", apiKey) + req.Header.Set("X-Timestamp", timestamp) + req.Header.Set("X-Signature", signature) + + // Send request + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("failed to send request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("wallet service returned status: %v", resp.Status) + } + + // Just decode the response to verify it's valid JSON but we don't need to process it + var result map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return fmt.Errorf("failed to decode response: %v", err) + } + + log.Printf("Successfully requested generation of %d addresses", count) + return nil +} + // getSubscriptionStatus returns the subscription status string func (m *SubscriptionManager) getSubscriptionStatus(activeTier string) string { if activeTier == "" { diff --git a/lib/web/handler_wallet_addresses.go b/lib/web/handler_wallet_addresses.go index 98708c4..bd5382b 100644 --- a/lib/web/handler_wallet_addresses.go +++ b/lib/web/handler_wallet_addresses.go @@ -6,117 +6,259 @@ import ( "log" "time" + "sync" + types "github.com/HORNET-Storage/hornet-storage/lib" "github.com/HORNET-Storage/hornet-storage/lib/stores" "github.com/gofiber/fiber/v2" "github.com/spf13/viper" ) -// Address status constants +// Constants for address statuses const ( AddressStatusAvailable = "available" AddressStatusAllocated = "allocated" AddressStatusUsed = "used" ) -// saveWalletAddresses processes incoming Bitcoin addresses and stores them for future -// subscriber allocation. These addresses will be used when subscribers initialize their -// subscription and need a payment address. +// saveWalletAddresses processes incoming Bitcoin addresses and stores them for future allocation. func saveWalletAddresses(c *fiber.Ctx, store stores.Store) error { log.Println("Addresses request received") - body := c.Body() - var addresses []types.Address - if err := json.Unmarshal(body, &addresses); err != nil { - log.Printf("Error unmarshaling JSON directly: %v", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Cannot parse JSON", - }) + if err := json.Unmarshal(c.Body(), &addresses); err != nil { + log.Printf("Error unmarshaling JSON: %v", err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cannot parse JSON"}) } expectedWalletName := viper.GetString("wallet_name") if expectedWalletName == "" { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "Wallet name not configured", - }) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Wallet name not configured"}) } - log.Printf("Expected wallet name: %s", expectedWalletName) - statsStore := store.GetStatsStore() - if statsStore == nil { - log.Println("Error: StatsStore is nil or not initialized") - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "StatsStore not available", - }) + subscriberStore := store.GetSubscriberStore() + if statsStore == nil || subscriberStore == nil { + log.Println("Error: StatsStore or SubscriberStore is nil or not initialized") + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Stores not available"}) } - log.Println("Successfully accessed StatsStore") - log.Println("Successfully accessed SubscriberStore") + const numWorkers = 5 + jobs := make(chan types.Address, len(addresses)) + results := make(chan error, len(addresses)) + var wg sync.WaitGroup + + // Worker function to process addresses + worker := func(jobs <-chan types.Address, results chan<- error) { + for addr := range jobs { + err := processAddress(addr, expectedWalletName, statsStore, subscriberStore) + results <- err + } + wg.Done() + } - processedCount := 0 + // Start worker pool + for i := 0; i < numWorkers; i++ { + wg.Add(1) + go worker(jobs, results) + } - // Process each address from the request + // Enqueue jobs for _, addr := range addresses { - if addr.WalletName != expectedWalletName { - log.Printf("Skipping address from unknown wallet: %s", addr.WalletName) - continue - } + jobs <- addr + } + close(jobs) - // Check if the address exists in StatsStore and save if it doesn't - existsInStatsStore, err := statsStore.AddressExists(addr.Address) + // Wait for all workers to finish + wg.Wait() + close(results) + + // Collect results + var processedCount, errorCount int + for err := range results { if err != nil { - log.Printf("Error checking address existence in StatsStore: %v", err) - continue + log.Printf("Error processing address: %v", err) + errorCount++ + } else { + processedCount++ } + } - // Save address to StatsStore if it doesn't exist - if !existsInStatsStore { - newStatsAddress := types.WalletAddress{ - Index: addr.Index, - Address: addr.Address, - } - log.Printf("Attempting to save new address to StatsStore: %v", newStatsAddress) - if err := statsStore.SaveAddress(&newStatsAddress); err != nil { - log.Printf("Error saving new address to StatsStore: %v", err) - continue - } - log.Printf("Address saved to StatsStore: %v", newStatsAddress) - } + return c.JSON(fiber.Map{ + "status": "success", + "message": fmt.Sprintf("Processed %d addresses successfully, %d errors", processedCount, errorCount), + }) +} - // Check if the address exists in SubscriberStore and save if it doesn't - existsInSubscriberStore, err := store.GetSubscriberStore().AddressExists(addr.Address) - if err != nil { - log.Printf("Error checking address existence in SubscriberStore: %v", err) - continue - } +// processAddress handles individual address processing, ensuring atomicity and reducing contention. +func processAddress(addr types.Address, expectedWalletName string, statsStore stores.StatisticsStore, subscriberStore stores.SubscriberStore) error { + if addr.WalletName != expectedWalletName { + log.Printf("Skipping address from unknown wallet: %s", addr.WalletName) + return nil + } - // Save WalletAddress to SubscriberStore if it doesn't exist - if !existsInSubscriberStore { - // Save Subscriber-specific data to SubscriberStore - subscriptionAddress := &types.SubscriberAddress{ - Index: fmt.Sprint(addr.Index), - Address: addr.Address, - WalletName: addr.WalletName, - Status: AddressStatusAvailable, - AllocatedAt: &time.Time{}, - Npub: nil, // Use nil for empty pointer - } - log.Printf("Attempting to save SubscriberAddress to SubscriberStore: %v", subscriptionAddress) - if err := store.GetSubscriberStore().SaveSubscriberAddress(subscriptionAddress); err != nil { - log.Printf("Error saving SubscriberAddress to SubscriberStore: %v", err) - continue - } - log.Printf("SubscriberAddress saved to SubscriberStore: %v", subscriptionAddress) + // Check and save address to StatsStore + existsInStatsStore, err := statsStore.AddressExists(addr.Address) + if err != nil { + log.Printf("Error checking address existence in StatsStore: %v", err) + return err + } + if !existsInStatsStore { + newStatsAddress := types.WalletAddress{ + Index: addr.Index, + Address: addr.Address, + } + if err := statsStore.SaveAddress(&newStatsAddress); err != nil { + log.Printf("Error saving new address to StatsStore: %v", err) + return err } + log.Printf("Address saved to StatsStore: %v", newStatsAddress) + } - processedCount++ + // Check and save address to SubscriberStore + existsInSubscriberStore, err := subscriberStore.AddressExists(addr.Address) + if err != nil { + log.Printf("Error checking address existence in SubscriberStore: %v", err) + return err + } + if !existsInSubscriberStore { + subscriptionAddress := &types.SubscriberAddress{ + Index: fmt.Sprint(addr.Index), + Address: addr.Address, + WalletName: addr.WalletName, + Status: AddressStatusAvailable, + AllocatedAt: &time.Time{}, + Npub: nil, + } + if err := subscriberStore.SaveSubscriberAddress(subscriptionAddress); err != nil { + log.Printf("Error saving SubscriberAddress to SubscriberStore: %v", err) + return err + } + log.Printf("SubscriberAddress saved to SubscriberStore: %v", subscriptionAddress) } - // Return success response with number of addresses processed - return c.JSON(fiber.Map{ - "status": "success", - "message": fmt.Sprintf("Processed %d addresses successfully", processedCount), - }) + return nil } + +// package web + +// import ( +// "encoding/json" +// "fmt" +// "log" +// "time" + +// types "github.com/HORNET-Storage/hornet-storage/lib" +// "github.com/HORNET-Storage/hornet-storage/lib/stores" +// "github.com/gofiber/fiber/v2" +// "github.com/spf13/viper" +// ) + +// // Address status constants +// const ( +// AddressStatusAvailable = "available" +// AddressStatusAllocated = "allocated" +// AddressStatusUsed = "used" +// ) + +// // saveWalletAddresses processes incoming Bitcoin addresses and stores them for future +// // subscriber allocation. These addresses will be used when subscribers initialize their +// // subscription and need a payment address. +// func saveWalletAddresses(c *fiber.Ctx, store stores.Store) error { +// log.Println("Addresses request received") + +// body := c.Body() + +// var addresses []types.Address +// if err := json.Unmarshal(body, &addresses); err != nil { +// log.Printf("Error unmarshaling JSON directly: %v", err) +// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ +// "error": "Cannot parse JSON", +// }) +// } + +// expectedWalletName := viper.GetString("wallet_name") +// if expectedWalletName == "" { +// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ +// "error": "Wallet name not configured", +// }) +// } + +// log.Printf("Expected wallet name: %s", expectedWalletName) + +// statsStore := store.GetStatsStore() +// if statsStore == nil { +// log.Println("Error: StatsStore is nil or not initialized") +// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ +// "error": "StatsStore not available", +// }) +// } +// log.Println("Successfully accessed StatsStore") + +// log.Println("Successfully accessed SubscriberStore") + +// processedCount := 0 + +// // Process each address from the request +// for _, addr := range addresses { +// if addr.WalletName != expectedWalletName { +// log.Printf("Skipping address from unknown wallet: %s", addr.WalletName) +// continue +// } + +// // Check if the address exists in StatsStore and save if it doesn't +// existsInStatsStore, err := statsStore.AddressExists(addr.Address) +// if err != nil { +// log.Printf("Error checking address existence in StatsStore: %v", err) +// continue +// } + +// // Save address to StatsStore if it doesn't exist +// if !existsInStatsStore { +// newStatsAddress := types.WalletAddress{ +// Index: addr.Index, +// Address: addr.Address, +// } +// log.Printf("Attempting to save new address to StatsStore: %v", newStatsAddress) +// if err := statsStore.SaveAddress(&newStatsAddress); err != nil { +// log.Printf("Error saving new address to StatsStore: %v", err) +// continue +// } +// log.Printf("Address saved to StatsStore: %v", newStatsAddress) +// } + +// // Check if the address exists in SubscriberStore and save if it doesn't +// existsInSubscriberStore, err := store.GetSubscriberStore().AddressExists(addr.Address) +// if err != nil { +// log.Printf("Error checking address existence in SubscriberStore: %v", err) +// continue +// } + +// // Save WalletAddress to SubscriberStore if it doesn't exist +// if !existsInSubscriberStore { +// // Save Subscriber-specific data to SubscriberStore +// subscriptionAddress := &types.SubscriberAddress{ +// Index: fmt.Sprint(addr.Index), +// Address: addr.Address, +// WalletName: addr.WalletName, +// Status: AddressStatusAvailable, +// AllocatedAt: &time.Time{}, +// Npub: nil, // Use nil for empty pointer +// } +// log.Printf("Attempting to save SubscriberAddress to SubscriberStore: %v", subscriptionAddress) +// if err := store.GetSubscriberStore().SaveSubscriberAddress(subscriptionAddress); err != nil { +// log.Printf("Error saving SubscriberAddress to SubscriberStore: %v", err) +// continue +// } +// log.Printf("SubscriberAddress saved to SubscriberStore: %v", subscriptionAddress) +// } + +// processedCount++ +// } + +// // Return success response with number of addresses processed +// return c.JSON(fiber.Map{ +// "status": "success", +// "message": fmt.Sprintf("Processed %d addresses successfully", processedCount), +// }) +// } From 60a33b846fa51059646b5618e57f48782b6b7a95 Mon Sep 17 00:00:00 2001 From: Maphikza Date: Tue, 12 Nov 2024 12:38:01 +0200 Subject: [PATCH 13/21] feat(settings): add subscription tiers support to relay settings - Add SubscriptionTier struct with data_limit and price fields - Update RelaySettings struct to include subscription_tiers field - Modify settings handlers to process subscription tiers data - Update viper config to handle subscription tiers storage - Ensure proper JSON serialization/deserialization of tier data --- lib/types.go | 43 ++++++++++++++++--------------- lib/web/handler_relay_settings.go | 9 +++++++ services/server/port/main.go | 6 ++--- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/lib/types.go b/lib/types.go index 9b913af..d143a86 100644 --- a/lib/types.go +++ b/lib/types.go @@ -169,23 +169,29 @@ type BitcoinRate struct { Timestamp time.Time `gorm:"autoUpdateTime"` // This will be updated each time the rate changes } +type SubscriptionTier struct { + DataLimit string `json:"data_limit" mapstructure:"data_limit"` + Price string `json:"price" mapstructure:"price"` +} + type RelaySettings struct { - Mode string `json:"mode"` - Protocol []string `json:"protocol"` - Kinds []string `json:"kinds"` - DynamicKinds []string `json:"dynamicKinds"` - Photos []string `json:"photos"` - Videos []string `json:"videos"` - GitNestr []string `json:"gitNestr"` - Audio []string `json:"audio"` - IsKindsActive bool `json:"isKindsActive"` - IsPhotosActive bool `json:"isPhotosActive"` - IsVideosActive bool `json:"isVideosActive"` - IsGitNestrActive bool `json:"isGitNestrActive"` - IsAudioActive bool `json:"isAudioActive"` - IsFileStorageActive bool `json:"isFileStorageActive"` - AppBuckets []string `json:"appBuckets"` - DynamicAppBuckets []string `json:"dynamicAppBuckets"` + Mode string `json:"mode"` + Protocol []string `json:"protocol"` + Kinds []string `json:"kinds"` + DynamicKinds []string `json:"dynamicKinds"` + Photos []string `json:"photos"` + Videos []string `json:"videos"` + GitNestr []string `json:"gitNestr"` + Audio []string `json:"audio"` + IsKindsActive bool `json:"isKindsActive"` + IsPhotosActive bool `json:"isPhotosActive"` + IsVideosActive bool `json:"isVideosActive"` + IsGitNestrActive bool `json:"isGitNestrActive"` + IsAudioActive bool `json:"isAudioActive"` + IsFileStorageActive bool `json:"isFileStorageActive"` + AppBuckets []string `json:"appBuckets"` + DynamicAppBuckets []string `json:"dynamicAppBuckets"` + SubscriptionTiers []SubscriptionTier `json:"subscription_tiers"` // New fields for the file type lists PhotoTypes []string `json:"photoTypes"` @@ -341,11 +347,6 @@ type Libp2pStream struct { Ctx context.Context } -type SubscriptionTier struct { - DataLimit string `mapstructure:"data_limit"` - Price string `mapstructure:"price"` -} - func (ls *Libp2pStream) Read(msg []byte) (int, error) { return ls.Stream.Read(msg) } diff --git a/lib/web/handler_relay_settings.go b/lib/web/handler_relay_settings.go index a6fdf60..471820e 100644 --- a/lib/web/handler_relay_settings.go +++ b/lib/web/handler_relay_settings.go @@ -99,6 +99,7 @@ func updateViperConfig(settings types.RelaySettings) error { viper.Set("relay_settings.Protocol", settings.Protocol) viper.Set("relay_settings.AppBuckets", settings.AppBuckets) viper.Set("relay_settings.DynamicAppBuckets", settings.DynamicAppBuckets) + viper.Set("subscription_tiers", settings.SubscriptionTiers) return viper.WriteConfig() } @@ -115,6 +116,7 @@ func getRelaySettings(c *fiber.Ctx) error { return c.Status(fiber.StatusInternalServerError).SendString("Failed to fetch settings") } + // Initialize empty slices if nil if relaySettings.Protocol == nil { relaySettings.Protocol = []string{} } @@ -127,6 +129,13 @@ func getRelaySettings(c *fiber.Ctx) error { relaySettings.DynamicAppBuckets = []string{} } + // Get subscription tiers + var subscriptionTiers []types.SubscriptionTier + if err := viper.UnmarshalKey("subscription_tiers", &subscriptionTiers); err != nil { + log.Printf("Error unmarshaling subscription tiers: %s", err) + } + relaySettings.SubscriptionTiers = subscriptionTiers + log.Println("Fetched relay settings:", relaySettings) return c.JSON(fiber.Map{ diff --git a/services/server/port/main.go b/services/server/port/main.go index 5cb36cc..bce6da8 100644 --- a/services/server/port/main.go +++ b/services/server/port/main.go @@ -132,15 +132,15 @@ func init() { viper.SetDefault("subscription_tiers", []map[string]interface{}{ { "data_limit": "1 GB per month", - "price": 10000, // in sats + "price": 8000, // in sats }, { "data_limit": "5 GB per month", - "price": 40000, // in sats + "price": 10000, // in sats }, { "data_limit": "10 GB per month", - "price": 70000, // in sats + "price": 15000, // in sats }, }) From e2404d331cb3cc3399426797da2217334edbef36 Mon Sep 17 00:00:00 2001 From: Maphikza Date: Tue, 12 Nov 2024 14:36:47 +0200 Subject: [PATCH 14/21] feat: Add conditional kind 411 event creation on subscription tier update - Enhanced function to conditionally create a new kind 411 event if there are changes in . - Passed as a parameter to and related route handlers. - Improved configuration update logic and comparison checks for to minimize unnecessary kind 411 creation. --- lib/web/handler_relay_settings.go | 46 ++++++++++++++++++++++++++++++- lib/web/server.go | 6 +++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/lib/web/handler_relay_settings.go b/lib/web/handler_relay_settings.go index 471820e..2f819c0 100644 --- a/lib/web/handler_relay_settings.go +++ b/lib/web/handler_relay_settings.go @@ -4,12 +4,15 @@ import ( "log" types "github.com/HORNET-Storage/hornet-storage/lib" + kind411creator "github.com/HORNET-Storage/hornet-storage/lib/handlers/nostr/kind411" + "github.com/HORNET-Storage/hornet-storage/lib/signing" + "github.com/HORNET-Storage/hornet-storage/lib/stores" "github.com/gofiber/fiber/v2" jsoniter "github.com/json-iterator/go" "github.com/spf13/viper" ) -func updateRelaySettings(c *fiber.Ctx) error { +func updateRelaySettings(c *fiber.Ctx, store stores.Store) error { log.Println("Relay settings request received") var json = jsoniter.ConfigCompatibleWithStandardLibrary var data map[string]interface{} @@ -38,6 +41,32 @@ func updateRelaySettings(c *fiber.Ctx) error { // Apply logic for boolean flags applyBooleanFlags(&relaySettings) + // Fetch existing subscription tiers from Viper + var existingTiers []types.SubscriptionTier + if err := viper.UnmarshalKey("subscription_tiers", &existingTiers); err != nil { + log.Println("Error fetching existing subscription tiers:", err) + } + + // Compare existing tiers with the new tiers + if tiersChanged(existingTiers, relaySettings.SubscriptionTiers) { + log.Println("Subscription tiers have changed, creating a new kind 411 event") + + serializedPrivateKey := viper.GetString("private_key") + + // Load private and public keys + privateKey, publicKey, err := signing.DeserializePrivateKey(serializedPrivateKey) + if err != nil { + log.Println("Error loading keys:", err) + return c.Status(fiber.StatusInternalServerError).SendString("Failed to load keys") + } + + // Create kind 411 event using the provided store instance + if err := kind411creator.CreateKind411Event(privateKey, publicKey, store); err != nil { + log.Println("Error creating kind 411 event:", err) + return c.Status(fiber.StatusInternalServerError).SendString("Failed to create kind 411 event") + } + } + // Update Viper configuration if err := updateViperConfig(relaySettings); err != nil { log.Printf("Error updating config: %s", err) @@ -142,3 +171,18 @@ func getRelaySettings(c *fiber.Ctx) error { "relay_settings": relaySettings, }) } + +// Function to compare existing tiers with new tiers +func tiersChanged(existing, new []types.SubscriptionTier) bool { + if len(existing) != len(new) { + return true + } + + for i := range existing { + if existing[i].DataLimit != new[i].DataLimit || existing[i].Price != new[i].Price { + return true + } + } + + return false +} diff --git a/lib/web/server.go b/lib/web/server.go index 1fdf7fe..7d8f0be 100644 --- a/lib/web/server.go +++ b/lib/web/server.go @@ -68,7 +68,11 @@ func StartServer(store stores.Store) error { secured.Get("/relaycount", func(c *fiber.Ctx) error { return getRelayCount(c, store) }) - secured.Post("/relay-settings", updateRelaySettings) + + secured.Post("/relay-settings", func(c *fiber.Ctx) error { + return updateRelaySettings(c, store) + }) + secured.Get("/relay-settings", getRelaySettings) secured.Get("/timeseries", func(c *fiber.Ctx) error { return getProfilesTimeSeriesData(c, store) From 4872a18e2b6c6d97e5c546f6d52729e161b3e0bd Mon Sep 17 00:00:00 2001 From: Maphikza Date: Tue, 12 Nov 2024 14:39:27 +0200 Subject: [PATCH 15/21] feat: Add conditional kind 411 event creation on subscription tier update - Enhanced updateRelaySettings function to conditionally create a new kind 411 event if there are changes in subscription_tiers. - Passed store as a parameter to updateRelaySettings and related route handlers. - Improved configuration update logic and comparison checks for subscription_tiers to minimize unnecessary kind 411 creation. --- lib/web/handler_relay_settings.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web/handler_relay_settings.go b/lib/web/handler_relay_settings.go index 2f819c0..5138796 100644 --- a/lib/web/handler_relay_settings.go +++ b/lib/web/handler_relay_settings.go @@ -63,7 +63,7 @@ func updateRelaySettings(c *fiber.Ctx, store stores.Store) error { // Create kind 411 event using the provided store instance if err := kind411creator.CreateKind411Event(privateKey, publicKey, store); err != nil { log.Println("Error creating kind 411 event:", err) - return c.Status(fiber.StatusInternalServerError).SendString("Failed to create kind 411 event") + return c.Status(fiber.StatusInternalServerError).SendString("Failed to create kind 411 event.") } } From 29f28c017f8dadba9408d8a4cb16f2d80ea2180a Mon Sep 17 00:00:00 2001 From: Maphikza Date: Wed, 13 Nov 2024 21:48:21 +0200 Subject: [PATCH 16/21] It is not necessary to update kind 411 when tiers change. we need to only update kind 764/888 --- lib/web/handler_relay_settings.go | 40 +------------------------------ lib/web/server.go | 6 +---- 2 files changed, 2 insertions(+), 44 deletions(-) diff --git a/lib/web/handler_relay_settings.go b/lib/web/handler_relay_settings.go index 5138796..721a47c 100644 --- a/lib/web/handler_relay_settings.go +++ b/lib/web/handler_relay_settings.go @@ -4,15 +4,12 @@ import ( "log" types "github.com/HORNET-Storage/hornet-storage/lib" - kind411creator "github.com/HORNET-Storage/hornet-storage/lib/handlers/nostr/kind411" - "github.com/HORNET-Storage/hornet-storage/lib/signing" - "github.com/HORNET-Storage/hornet-storage/lib/stores" "github.com/gofiber/fiber/v2" jsoniter "github.com/json-iterator/go" "github.com/spf13/viper" ) -func updateRelaySettings(c *fiber.Ctx, store stores.Store) error { +func updateRelaySettings(c *fiber.Ctx) error { log.Println("Relay settings request received") var json = jsoniter.ConfigCompatibleWithStandardLibrary var data map[string]interface{} @@ -47,26 +44,6 @@ func updateRelaySettings(c *fiber.Ctx, store stores.Store) error { log.Println("Error fetching existing subscription tiers:", err) } - // Compare existing tiers with the new tiers - if tiersChanged(existingTiers, relaySettings.SubscriptionTiers) { - log.Println("Subscription tiers have changed, creating a new kind 411 event") - - serializedPrivateKey := viper.GetString("private_key") - - // Load private and public keys - privateKey, publicKey, err := signing.DeserializePrivateKey(serializedPrivateKey) - if err != nil { - log.Println("Error loading keys:", err) - return c.Status(fiber.StatusInternalServerError).SendString("Failed to load keys") - } - - // Create kind 411 event using the provided store instance - if err := kind411creator.CreateKind411Event(privateKey, publicKey, store); err != nil { - log.Println("Error creating kind 411 event:", err) - return c.Status(fiber.StatusInternalServerError).SendString("Failed to create kind 411 event.") - } - } - // Update Viper configuration if err := updateViperConfig(relaySettings); err != nil { log.Printf("Error updating config: %s", err) @@ -171,18 +148,3 @@ func getRelaySettings(c *fiber.Ctx) error { "relay_settings": relaySettings, }) } - -// Function to compare existing tiers with new tiers -func tiersChanged(existing, new []types.SubscriptionTier) bool { - if len(existing) != len(new) { - return true - } - - for i := range existing { - if existing[i].DataLimit != new[i].DataLimit || existing[i].Price != new[i].Price { - return true - } - } - - return false -} diff --git a/lib/web/server.go b/lib/web/server.go index 7d8f0be..1fdf7fe 100644 --- a/lib/web/server.go +++ b/lib/web/server.go @@ -68,11 +68,7 @@ func StartServer(store stores.Store) error { secured.Get("/relaycount", func(c *fiber.Ctx) error { return getRelayCount(c, store) }) - - secured.Post("/relay-settings", func(c *fiber.Ctx) error { - return updateRelaySettings(c, store) - }) - + secured.Post("/relay-settings", updateRelaySettings) secured.Get("/relay-settings", getRelaySettings) secured.Get("/timeseries", func(c *fiber.Ctx) error { return getProfilesTimeSeriesData(c, store) From 7f9641b2b2e48674f2018dff836949ad701476f4 Mon Sep 17 00:00:00 2001 From: Maphikza Date: Thu, 14 Nov 2024 11:47:43 +0200 Subject: [PATCH 17/21] changing kind 764 to 888 --- lib/handlers/scionic/utils.go | 2 +- lib/subscription/subscription.go | 14 +- lib/web/handler_update_wallet_transactions.go | 444 +----------------- 3 files changed, 12 insertions(+), 448 deletions(-) diff --git a/lib/handlers/scionic/utils.go b/lib/handlers/scionic/utils.go index fc39dc7..cb5a7ca 100644 --- a/lib/handlers/scionic/utils.go +++ b/lib/handlers/scionic/utils.go @@ -222,7 +222,7 @@ func LoadRelaySettings() (*types.RelaySettings, error) { func ValidateUploadEligibility(store stores.Store, npub string, data []byte) error { // Step 1: Fetch the NIP-88 event for the given subscriber events, err := store.QueryEvents(nostr.Filter{ - Kinds: []int{764}, // Assuming 764 is the NIP-88 kind + Kinds: []int{888}, // Assuming 888 is the NIP-88 kind Tags: nostr.TagMap{ "p": []string{npub}, }, diff --git a/lib/subscription/subscription.go b/lib/subscription/subscription.go index 79996f8..0d198ce 100644 --- a/lib/subscription/subscription.go +++ b/lib/subscription/subscription.go @@ -105,7 +105,7 @@ func (m *SubscriptionManager) ProcessPayment( // Step 2: Fetch NIP-88 event data to retrieve subscriber information events, err := m.store.QueryEvents(nostr.Filter{ - Kinds: []int{764}, + Kinds: []int{888}, Tags: nostr.TagMap{ "p": []string{npub}, }, @@ -146,7 +146,7 @@ func (m *SubscriptionManager) ProcessPayment( func (m *SubscriptionManager) UpdateStorageUsage(npub string, newBytes int64) error { // Fetch current NIP-88 event data events, err := m.store.QueryEvents(nostr.Filter{ - Kinds: []int{764}, + Kinds: []int{888}, Tags: nostr.TagMap{ "p": []string{npub}, }, @@ -184,7 +184,7 @@ func (m *SubscriptionManager) UpdateStorageUsage(npub string, newBytes int64) er func (m *SubscriptionManager) CheckStorageAvailability(npub string, requestedBytes int64) error { // Step 1: Fetch the user's NIP-88 event events, err := m.store.QueryEvents(nostr.Filter{ - Kinds: []int{764}, + Kinds: []int{888}, Tags: nostr.TagMap{ "p": []string{npub}, }, @@ -234,7 +234,7 @@ func (m *SubscriptionManager) createOrUpdateNIP88Event( ) error { // Delete existing NIP-88 event if it exists existingEvents, err := m.store.QueryEvents(nostr.Filter{ - Kinds: []int{764}, + Kinds: []int{888}, Tags: nostr.TagMap{ "p": []string{subscriber.Npub}, }, @@ -265,7 +265,7 @@ func (m *SubscriptionManager) createOrUpdateNIP88Event( event := &nostr.Event{ PubKey: hex.EncodeToString(m.relayPrivateKey.PubKey().SerializeCompressed()), CreatedAt: nostr.Timestamp(time.Now().Unix()), - Kind: 764, + Kind: 888, Tags: tags, Content: "", } @@ -292,7 +292,7 @@ func (m *SubscriptionManager) createNIP88EventIfNotExists( ) error { // Check if an existing NIP-88 event for the subscriber already exists existingEvents, err := m.store.QueryEvents(nostr.Filter{ - Kinds: []int{764}, // Assuming 764 is the NIP-88 event kind + Kinds: []int{888}, // Assuming 888 is the NIP-88 event kind Tags: nostr.TagMap{ "p": []string{subscriber.Npub}, }, @@ -327,7 +327,7 @@ func (m *SubscriptionManager) createNIP88EventIfNotExists( event := &nostr.Event{ PubKey: hex.EncodeToString(m.relayPrivateKey.PubKey().SerializeCompressed()), CreatedAt: nostr.Timestamp(time.Now().Unix()), - Kind: 764, + Kind: 888, Tags: tags, Content: "", } diff --git a/lib/web/handler_update_wallet_transactions.go b/lib/web/handler_update_wallet_transactions.go index 584c81c..6f530a3 100644 --- a/lib/web/handler_update_wallet_transactions.go +++ b/lib/web/handler_update_wallet_transactions.go @@ -106,10 +106,13 @@ func processTransaction(store stores.Store, subManager *subscription.Subscriptio return fmt.Errorf("failed to save transaction: %v", err) } - // Get subscriber by their Bitcoin address + // After subscriber retrieval in processTransaction subscriber, err := store.GetSubscriberByAddress(txDetails.output) if err != nil { + log.Printf("Error: subscriber not found for address %s: %v", txDetails.output, err) return fmt.Errorf("subscriber not found for address %s: %v", txDetails.output, err) + } else { + log.Printf("Subscriber retrieved: %v", subscriber) } // Convert BTC value to satoshis for subscription processing @@ -210,442 +213,3 @@ func initializeSubscriptionManager(store stores.Store) (*subscription.Subscripti subscriptionTiers, ), nil } - -// package web - -// import ( -// "crypto/sha256" -// "encoding/hex" -// "fmt" -// "log" -// "strconv" -// "strings" -// "time" - -// "github.com/btcsuite/btcd/btcec/v2" -// "github.com/btcsuite/btcd/btcec/v2/schnorr" -// "github.com/gofiber/fiber/v2" -// "github.com/nbd-wtf/go-nostr" -// "github.com/spf13/viper" - -// types "github.com/HORNET-Storage/hornet-storage/lib" -// "github.com/HORNET-Storage/hornet-storage/lib/signing" -// "github.com/HORNET-Storage/hornet-storage/lib/stores" -// ) - -// func updateWalletTransactions(c *fiber.Ctx, store stores.Store) error { -// var transactions []map[string]interface{} -// log.Println("Transactions request received") - -// // Parse the JSON body into the slice of maps -// if err := c.BodyParser(&transactions); err != nil { -// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ -// "error": "Cannot parse JSON", -// }) -// } - -// // Get the expected wallet name from the configuration -// expectedWalletName := viper.GetString("wallet_name") - -// // Set wallet name from first transaction if not set -// if expectedWalletName == "" && len(transactions) > 0 { -// walletName, ok := transactions[0]["wallet_name"].(string) -// if !ok { -// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ -// "error": "Wallet name missing or invalid", -// }) -// } -// viper.Set("wallet_name", walletName) -// expectedWalletName = walletName -// } - -// for _, transaction := range transactions { -// walletName, ok := transaction["wallet_name"].(string) -// if !ok || walletName != expectedWalletName { -// continue // Skip processing if wallet name doesn't match -// } - -// // Extract transaction details -// address, _ := transaction["address"].(string) -// dateStr, _ := transaction["date"].(string) -// date, err := time.Parse(time.RFC3339, dateStr) -// if err != nil { -// log.Printf("Error parsing date: %v", err) -// continue -// } -// output, _ := transaction["output"].(string) -// valueStr, _ := transaction["value"].(string) -// value, err := strconv.ParseFloat(valueStr, 64) -// if err != nil { -// log.Printf("Error parsing value: %v", err) -// continue -// } - -// // Process pending transactions -// txID := strings.Split(address, ":")[0] -// err = store.GetStatsStore().DeletePendingTransaction(txID) -// if err != nil { -// continue -// } - -// // Check for existing transactions -// exists, err := store.GetStatsStore().TransactionExists(address, date, output, valueStr) -// if err != nil { -// log.Printf("Error checking existing transactions: %v", err) -// continue -// } -// if exists { -// continue -// } - -// // Create a new transaction -// newTransaction := types.WalletTransactions{ -// Address: address, -// Date: date, -// Output: output, -// Value: fmt.Sprintf("%.8f", value), -// } - -// err = store.GetStatsStore().SaveWalletTransaction(newTransaction) -// if err != nil { -// log.Printf("Error saving new transaction: %v", err) -// continue -// } - -// // Process subscription payments -// err = processSubscriptionPayment(store, transaction) -// if err != nil { -// log.Printf("Error processing subscription payment: %v", err) -// } -// } - -// return c.JSON(fiber.Map{ -// "status": "success", -// "message": "Transactions processed successfully", -// }) -// } - -// func processSubscriptionPayment(store stores.Store, transaction map[string]interface{}) error { -// log.Printf("Processing transaction: %+v", transaction) - -// // Get subscription tiers from config -// var subscriptionTiers []types.SubscriptionTier -// if err := viper.UnmarshalKey("subscription_tiers", &subscriptionTiers); err != nil { -// return fmt.Errorf("failed to fetch subscription tiers: %v", err) -// } - -// // Log subscription tiers to confirm they are loaded correctly -// for _, tier := range subscriptionTiers { -// log.Printf("Loaded subscription tier: DataLimit=%s, Price=%s", tier.DataLimit, tier.Price) -// } - -// // Extract and validate the Bitcoin address -// output, ok := transaction["output"].(string) -// if !ok { -// return fmt.Errorf("invalid output in transaction") -// } -// log.Printf("Looking up subscriber for address: %s", output) - -// // Debug: Check if address exists in subscriber_addresses table -// if store, ok := store.(interface { -// AddressExists(string) (bool, error) -// }); ok { -// exists, err := store.AddressExists(output) -// if err != nil { -// log.Printf("Error checking address existence: %v", err) -// } else { -// log.Printf("Address %s exists in database: %v", output, exists) -// } -// } - -// // Debug: Check address allocation status -// if store, ok := store.(interface { -// DebugAddressDetails(string) -// }); ok { -// log.Printf("Checking address details for: %s", output) -// store.DebugAddressDetails(output) -// } - -// // Get subscriber details -// subscriber, err := store.GetSubscriberByAddress(output) -// if err != nil { -// log.Printf("Failed to find subscriber for address %s: %v", output, err) -// // Dump the subscriber_addresses table contents for debugging -// if store, ok := store.(interface { -// DumpAddressTable() -// }); ok { -// store.DumpAddressTable() -// } -// return fmt.Errorf("subscriber not found: %v", err) -// } - -// // Validate and parse transaction details -// transactionID, valueStr, err := validateTransactionDetails(transaction, subscriber) -// if err != nil { -// return err -// } - -// // Find matching tier for payment amount -// matchedTier, err := findMatchingTier(valueStr, subscriptionTiers) -// if err != nil { -// return err -// } -// if matchedTier == nil { -// log.Printf("Transaction value %v does not match any subscription tier for address: %s", valueStr, output) -// return nil -// } - -// // Log to confirm the DataLimit value -// log.Printf("Matched tier data limit: %s", matchedTier.DataLimit) - -// // Update subscription details -// newEndDate := calculateNewEndDate(subscriber.EndDate) -// storageLimitBytes, err := stores.ParseStorageLimit(matchedTier.DataLimit) -// if err != nil { -// return fmt.Errorf("failed to parse storage limit: %v", err) -// } - -// // Update subscriber record -// if err := updateSubscriberRecord(store, subscriber, matchedTier, transactionID, newEndDate, storageLimitBytes); err != nil { -// return err -// } - -// // Update NIP-88 event -// if err := updateNIP88Event(store, subscriber, matchedTier, newEndDate); err != nil { -// log.Printf("Warning: NIP-88 event update failed: %v", err) -// // Continue despite NIP-88 error as the subscription is already updated -// } - -// log.Printf("Subscriber %s activated/extended on tier %s with transaction ID %s. New end date: %v", -// subscriber.Npub, matchedTier.DataLimit, transactionID, newEndDate) - -// return nil -// } - -// func validateTransactionDetails(transaction map[string]interface{}, subscriber *types.Subscriber) (string, string, error) { -// txAddressField, ok := transaction["address"].(string) -// if !ok || txAddressField == "" { -// return "", "", fmt.Errorf("transaction ID missing or invalid") -// } -// transactionID := strings.Split(txAddressField, ":")[0] - -// if subscriber.LastTransactionID == transactionID { -// log.Printf("Transaction ID %s has already been processed for subscriber %s", transactionID, subscriber.Npub) -// return "", "", fmt.Errorf("transaction already processed") -// } - -// valueStr, ok := transaction["value"].(string) -// if !ok { -// return "", "", fmt.Errorf("transaction value missing or invalid") -// } - -// return transactionID, valueStr, nil -// } - -// func findMatchingTier(valueStr string, tiers []types.SubscriptionTier) (*types.SubscriptionTier, error) { -// // Parse the BTC value as a float -// value, err := strconv.ParseFloat(valueStr, 64) -// if err != nil { -// return nil, fmt.Errorf("error parsing transaction value: %v", err) -// } - -// // Convert the value from BTC to satoshis (1 BTC = 100,000,000 satoshis) -// paymentSats := int64(value * 100_000_000) -// log.Printf("Processing payment of %d satoshis", paymentSats) - -// var bestMatch *types.SubscriptionTier -// var bestPrice int64 = 0 - -// for _, tier := range tiers { -// log.Printf("Checking tier: DataLimit=%s, Price=%s", tier.DataLimit, tier.Price) - -// // Parse the tier price as an integer in satoshis -// tierPrice, err := strconv.ParseInt(tier.Price, 10, 64) -// if err != nil { -// log.Printf("Warning: invalid tier price configuration: %v", err) -// continue -// } - -// // Check if the payment meets or exceeds the tier price, and if it’s the highest eligible price -// if paymentSats >= tierPrice && tierPrice > bestPrice { -// tierCopy := tier // Copy the struct to avoid pointer issues -// bestMatch = &tierCopy -// bestPrice = tierPrice -// log.Printf("Found matching tier: %s (price: %d sats)", tier.DataLimit, tierPrice) -// } -// } - -// if bestMatch != nil { -// log.Printf("Selected tier: %s for payment of %d satoshis", bestMatch.DataLimit, paymentSats) -// } else { -// log.Printf("No matching tier found for payment of %d satoshis", paymentSats) -// } - -// return bestMatch, nil -// } - -// func calculateNewEndDate(currentEndDate time.Time) time.Time { -// if time.Now().Before(currentEndDate) { -// return currentEndDate.AddDate(0, 1, 0) -// } -// return time.Now().AddDate(0, 1, 0) -// } - -// func updateSubscriberRecord(store stores.Store, subscriber *types.Subscriber, tier *types.SubscriptionTier, -// transactionID string, endDate time.Time, storageLimitBytes int64) error { - -// log.Println("Updating subscriber: ", subscriber.Npub) -// subscriber.Tier = tier.DataLimit -// subscriber.StartDate = time.Now() -// subscriber.EndDate = endDate -// subscriber.LastTransactionID = transactionID - -// if subscriberStore, ok := store.(stores.SubscriberStore); ok { -// period := &types.SubscriptionPeriod{ -// TransactionID: transactionID, -// Tier: tier.DataLimit, -// StorageLimitBytes: storageLimitBytes, -// StartDate: time.Now(), -// EndDate: endDate, -// PaymentAmount: tier.Price, -// } -// if err := subscriberStore.AddSubscriptionPeriod(subscriber.Npub, period); err != nil { -// return fmt.Errorf("failed to add subscription period: %v", err) -// } -// } - -// err := store.DeleteSubscriber(subscriber.Npub) -// if err != nil { -// return fmt.Errorf("failed to delete subscriber: %v", err) -// } - -// newSubscriberEntry := &types.Subscriber{ -// Npub: subscriber.Npub, -// Tier: tier.DataLimit, -// StartDate: time.Now(), -// EndDate: endDate, -// Address: subscriber.Address, -// LastTransactionID: transactionID, -// } - -// return store.SaveSubscriber(newSubscriberEntry) -// } - -// func updateNIP88Event(store stores.Store, subscriber *types.Subscriber, tier *types.SubscriptionTier, endDate time.Time) error { -// relayPrivKey, _, err := loadRelayPrivateKey() -// if err != nil { -// return fmt.Errorf("failed to load relay private key: %v", err) -// } - -// return UpdateNIP88EventAfterPayment(relayPrivKey, subscriber.Npub, store, tier.DataLimit, endDate.Unix()) -// } - -// func UpdateNIP88EventAfterPayment(relayPrivKey *btcec.PrivateKey, userPubKey string, store stores.Store, tier string, expirationTimestamp int64) error { -// existingEvent, err := getExistingNIP88Event(store, userPubKey) -// if err != nil { -// return fmt.Errorf("error fetching existing NIP-88 event: %v", err) -// } -// if existingEvent == nil { -// return fmt.Errorf("no existing NIP-88 event found for user") -// } - -// // Delete the existing event -// err = store.DeleteEvent(existingEvent.ID) -// if err != nil { -// return fmt.Errorf("error deleting existing NIP-88 event: %v", err) -// } - -// subscriptionTiers := []types.SubscriptionTier{ -// {DataLimit: "1 GB per month", Price: "8000"}, -// {DataLimit: "5 GB per month", Price: "10000"}, -// {DataLimit: "10 GB per month", Price: "15000"}, -// } - -// var relayAddress string -// for _, tag := range existingEvent.Tags { -// if tag[0] == "relay_bitcoin_address" && len(tag) > 1 { -// relayAddress = tag[1] -// break -// } -// } - -// tags := []nostr.Tag{ -// {"subscription_duration", "1 month"}, -// {"p", userPubKey}, -// {"subscription_status", "active"}, -// {"relay_bitcoin_address", relayAddress}, -// {"relay_dht_key", viper.GetString("RelayDHTkey")}, -// {"active_subscription", tier, fmt.Sprintf("%d", expirationTimestamp)}, -// } - -// for _, tier := range subscriptionTiers { -// tags = append(tags, nostr.Tag{"subscription-tier", tier.DataLimit, tier.Price}) -// } - -// serializedPrivateKey, err := signing.SerializePrivateKey(relayPrivKey) -// if err != nil { -// log.Printf("failed to serialize private key") -// } - -// event := &nostr.Event{ -// PubKey: *serializedPrivateKey, -// CreatedAt: nostr.Timestamp(time.Now().Unix()), -// Kind: 764, -// Tags: tags, -// Content: "", -// } - -// // Generate the event ID -// serializedEvent := event.Serialize() -// hash := sha256.Sum256(serializedEvent) -// event.ID = hex.EncodeToString(hash[:]) - -// // Sign the event -// sig, err := schnorr.Sign(relayPrivKey, hash[:]) -// if err != nil { -// return fmt.Errorf("error signing event: %v", err) -// } -// event.Sig = hex.EncodeToString(sig.Serialize()) - -// log.Println("Storing updated kind 764 event") - -// // Store the event -// err = store.StoreEvent(event) -// if err != nil { -// return fmt.Errorf("failed to store NIP-88 event: %v", err) -// } - -// log.Println("Kind 764 event successfully stored.") - -// return nil -// } - -// func getExistingNIP88Event(store stores.Store, userPubKey string) (*nostr.Event, error) { -// filter := nostr.Filter{ -// Kinds: []int{764}, -// Tags: nostr.TagMap{ -// "p": []string{userPubKey}, -// }, -// Limit: 1, -// } - -// events, err := store.QueryEvents(filter) -// if err != nil { -// return nil, err -// } - -// if len(events) > 0 { -// return events[0], nil -// } - -// return nil, nil -// } - -// func loadRelayPrivateKey() (*btcec.PrivateKey, *btcec.PublicKey, error) { -// privateKey, publicKey, err := signing.DeserializePrivateKey(viper.GetString("priv_key")) -// if err != nil { -// return nil, nil, fmt.Errorf("error getting keys: %s", err) -// } - -// return privateKey, publicKey, nil -// } From d9815c37fed1bbf2adaf596be33841319d3397a3 Mon Sep 17 00:00:00 2001 From: Maphikza Date: Thu, 14 Nov 2024 15:49:41 +0200 Subject: [PATCH 18/21] feat: Add dynamic subscription_tier tags to NIP-88 events - Updated createNIP88EventIfNotExists and createOrUpdateNIP88Event functions to include dynamic 'subscription_tier' tags based on values retrieved via Viper configuration. - Implemented type assertions and error handling for retrieving and parsing 'subscription_tiers' from configuration. - Ensured consistent tagging for tiers, including data limits and prices, with improved error logging for type conversions and assertions. --- lib/subscription/subscription.go | 54 ++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/lib/subscription/subscription.go b/lib/subscription/subscription.go index 0d198ce..d738688 100644 --- a/lib/subscription/subscription.go +++ b/lib/subscription/subscription.go @@ -256,6 +256,33 @@ func (m *SubscriptionManager) createOrUpdateNIP88Event( {"storage", fmt.Sprintf("%d", storageInfo.UsedBytes), fmt.Sprintf("%d", storageInfo.TotalBytes), fmt.Sprintf("%d", storageInfo.UpdatedAt.Unix())}, } + // Fetch and add subscription_tier tags based on values from Viper + rawTiers := viper.Get("subscription_tiers") + if rawTiers != nil { + if tiers, ok := rawTiers.([]interface{}); ok { + for _, tier := range tiers { + if tierMap, ok := tier.(map[string]interface{}); ok { + dataLimit, okDataLimit := tierMap["data_limit"].(string) + price, okPrice := tierMap["price"].(string) + if okDataLimit && okPrice { + priceInt, err := strconv.Atoi(price) // Convert string price to integer + if err != nil { + log.Printf("error converting price %s to integer: %v", price, err) + continue + } + tags = append(tags, nostr.Tag{"subscription_tier", dataLimit, strconv.Itoa(priceInt)}) + } else { + log.Printf("invalid data structure for tier: %v", tierMap) + } + } else { + log.Printf("error asserting tier to map[string]interface{}: %v", tier) + } + } + } else { + log.Printf("error asserting subscription_tiers to []interface{}: %v", rawTiers) + } + } + if activeTier != "" { tags = append(tags, nostr.Tag{ "active_subscription", activeTier, fmt.Sprintf("%d", expirationDate.Unix()), @@ -318,6 +345,33 @@ func (m *SubscriptionManager) createNIP88EventIfNotExists( {"storage", fmt.Sprintf("%d", storageInfo.UsedBytes), fmt.Sprintf("%d", storageInfo.TotalBytes), fmt.Sprintf("%d", storageInfo.UpdatedAt.Unix())}, } + // Fetch and add subscription_tier tags based on the values from Viper + rawTiers := viper.Get("subscription_tiers") + if rawTiers != nil { + if tiers, ok := rawTiers.([]interface{}); ok { + for _, tier := range tiers { + if tierMap, ok := tier.(map[string]interface{}); ok { + dataLimit, okDataLimit := tierMap["data_limit"].(string) + price, okPrice := tierMap["price"].(string) + if okDataLimit && okPrice { + priceInt, err := strconv.Atoi(price) // Convert string price to integer + if err != nil { + log.Printf("error converting price %s to integer: %v", price, err) + continue + } + tags = append(tags, nostr.Tag{"subscription_tier", dataLimit, strconv.Itoa(priceInt)}) + } else { + log.Printf("invalid data structure for tier: %v", tierMap) + } + } else { + log.Printf("error asserting tier to map[string]interface{}: %v", tier) + } + } + } else { + log.Printf("error asserting subscription_tiers to []interface{}: %v", rawTiers) + } + } + if activeTier != "" { tags = append(tags, nostr.Tag{ "active_subscription", activeTier, fmt.Sprintf("%d", expirationDate.Unix()), From 89653d93776c247d7cb113fe59d7dad265763b73 Mon Sep 17 00:00:00 2001 From: Maphikza Date: Fri, 15 Nov 2024 11:04:33 +0200 Subject: [PATCH 19/21] Since kind 888 will have a lot of copies updating the subscription tiers when the relay runner needs to make an adjustment to the tier prices it will take too much effort changing all the kind 888 to reflect this change so if we just update it on kind 411 then subscribers can pull the updated pricing fron 411 --- .../kind411/subscriptionEventsCreator.go | 51 +++++++++++++------ lib/web/handler_relay_settings.go | 39 +++++++++++++- lib/web/server.go | 5 +- 3 files changed, 77 insertions(+), 18 deletions(-) diff --git a/lib/handlers/nostr/kind411/subscriptionEventsCreator.go b/lib/handlers/nostr/kind411/subscriptionEventsCreator.go index 5cbf2ff..d3973d9 100644 --- a/lib/handlers/nostr/kind411/subscriptionEventsCreator.go +++ b/lib/handlers/nostr/kind411/subscriptionEventsCreator.go @@ -26,14 +26,15 @@ const ( ) type RelayInfo struct { - Name string `json:"name"` - Description string `json:"description,omitempty"` - Pubkey string `json:"pubkey"` - Contact string `json:"contact"` - SupportedNIPs []int `json:"supported_nips"` - Software string `json:"software"` - Version string `json:"version"` - DHTkey string `json:"dhtkey,omitempty"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Pubkey string `json:"pubkey"` + Contact string `json:"contact"` + SupportedNIPs []int `json:"supported_nips"` + Software string `json:"software"` + Version string `json:"version"` + DHTkey string `json:"dhtkey,omitempty"` + SubscriptionTiers []map[string]interface{} `json:"subscription_tiers,omitempty"` // New field } func CreateKind411Event(privateKey *secp256k1.PrivateKey, publicKey *secp256k1.PublicKey, store stores.Store) error { @@ -56,16 +57,34 @@ func CreateKind411Event(privateKey *secp256k1.PrivateKey, publicKey *secp256k1.P } } + // Retrieve subscription tiers from Viper + var subscriptionTiers []map[string]interface{} + rawTiers := viper.Get("subscription_tiers") + if rawTiers != nil { + if tiers, ok := rawTiers.([]interface{}); ok { + for _, tier := range tiers { + if tierMap, ok := tier.(map[string]interface{}); ok { + subscriptionTiers = append(subscriptionTiers, tierMap) + } else { + log.Printf("error asserting tier to map[string]interface{}: %v", tier) + } + } + } else { + log.Printf("error asserting subscription_tiers to []interface{}: %v", rawTiers) + } + } + // Get relay info relayInfo := RelayInfo{ - Name: viper.GetString("RelayName"), - Description: viper.GetString("RelayDescription"), - Pubkey: viper.GetString("RelayPubkey"), - Contact: viper.GetString("RelayContact"), - SupportedNIPs: []int{1, 11, 2, 9, 18, 23, 24, 25, 51, 56, 57, 42, 45, 50, 65, 116}, - Software: viper.GetString("RelaySoftware"), - Version: viper.GetString("RelayVersion"), - DHTkey: viper.GetString("RelayDHTkey"), + Name: viper.GetString("RelayName"), + Description: viper.GetString("RelayDescription"), + Pubkey: viper.GetString("RelayPubkey"), + Contact: viper.GetString("RelayContact"), + SupportedNIPs: []int{1, 11, 2, 9, 18, 23, 24, 25, 51, 56, 57, 42, 45, 50, 65, 116}, + Software: viper.GetString("RelaySoftware"), + Version: viper.GetString("RelayVersion"), + DHTkey: viper.GetString("RelayDHTkey"), + SubscriptionTiers: subscriptionTiers, } // Convert relay info to JSON diff --git a/lib/web/handler_relay_settings.go b/lib/web/handler_relay_settings.go index 721a47c..564c6e0 100644 --- a/lib/web/handler_relay_settings.go +++ b/lib/web/handler_relay_settings.go @@ -4,12 +4,15 @@ import ( "log" types "github.com/HORNET-Storage/hornet-storage/lib" + kind411creator "github.com/HORNET-Storage/hornet-storage/lib/handlers/nostr/kind411" + "github.com/HORNET-Storage/hornet-storage/lib/signing" + "github.com/HORNET-Storage/hornet-storage/lib/stores" "github.com/gofiber/fiber/v2" jsoniter "github.com/json-iterator/go" "github.com/spf13/viper" ) -func updateRelaySettings(c *fiber.Ctx) error { +func updateRelaySettings(c *fiber.Ctx, store stores.Store) error { log.Println("Relay settings request received") var json = jsoniter.ConfigCompatibleWithStandardLibrary var data map[string]interface{} @@ -44,6 +47,25 @@ func updateRelaySettings(c *fiber.Ctx) error { log.Println("Error fetching existing subscription tiers:", err) } + // Compare existing tiers with the new tiers + if tiersChanged(existingTiers, relaySettings.SubscriptionTiers) { + log.Println("Subscription tiers have changed, creating a new kind 411 event") + + serializedPrivateKey := viper.GetString("private_key") + // Load private and public keys + privateKey, publicKey, err := signing.DeserializePrivateKey(serializedPrivateKey) // Assume a function to load private and public keys + if err != nil { + log.Println("Error loading keys:", err) + return c.Status(fiber.StatusInternalServerError).SendString("Failed to load keys") + } + + // Create kind 411 event using the provided store instance + if err := kind411creator.CreateKind411Event(privateKey, publicKey, store); err != nil { + log.Println("Error creating kind 411 event:", err) + return c.Status(fiber.StatusInternalServerError).SendString("Failed to create kind 411 event") + } + } + // Update Viper configuration if err := updateViperConfig(relaySettings); err != nil { log.Printf("Error updating config: %s", err) @@ -55,6 +77,21 @@ func updateRelaySettings(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) } +// Function to compare existing tiers with new tiers +func tiersChanged(existing, new []types.SubscriptionTier) bool { + if len(existing) != len(new) { + return true + } + + for i := range existing { + if existing[i].DataLimit != new[i].DataLimit || existing[i].Price != new[i].Price { + return true + } + } + + return false +} + func applyBooleanFlags(settings *types.RelaySettings) { if !settings.IsKindsActive { settings.Kinds = []string{} diff --git a/lib/web/server.go b/lib/web/server.go index 1fdf7fe..fbc5d6d 100644 --- a/lib/web/server.go +++ b/lib/web/server.go @@ -68,7 +68,10 @@ func StartServer(store stores.Store) error { secured.Get("/relaycount", func(c *fiber.Ctx) error { return getRelayCount(c, store) }) - secured.Post("/relay-settings", updateRelaySettings) + secured.Post("/relay-settings", func(c *fiber.Ctx) error { + return updateRelaySettings(c, store) + }) + secured.Get("/relay-settings", getRelaySettings) secured.Get("/timeseries", func(c *fiber.Ctx) error { return getProfilesTimeSeriesData(c, store) From b022fad35d27131ed482ce03a64fe4ae912bfa4a Mon Sep 17 00:00:00 2001 From: Maphikza Date: Fri, 15 Nov 2024 15:57:29 +0200 Subject: [PATCH 20/21] updating kind 411 to correctly handle subscription tiers --- .../kind411/subscriptionEventsCreator.go | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/lib/handlers/nostr/kind411/subscriptionEventsCreator.go b/lib/handlers/nostr/kind411/subscriptionEventsCreator.go index d3973d9..7169bfd 100644 --- a/lib/handlers/nostr/kind411/subscriptionEventsCreator.go +++ b/lib/handlers/nostr/kind411/subscriptionEventsCreator.go @@ -57,21 +57,12 @@ func CreateKind411Event(privateKey *secp256k1.PrivateKey, publicKey *secp256k1.P } } - // Retrieve subscription tiers from Viper + // Retrieve subscription tiers from Viper using UnmarshalKey var subscriptionTiers []map[string]interface{} - rawTiers := viper.Get("subscription_tiers") - if rawTiers != nil { - if tiers, ok := rawTiers.([]interface{}); ok { - for _, tier := range tiers { - if tierMap, ok := tier.(map[string]interface{}); ok { - subscriptionTiers = append(subscriptionTiers, tierMap) - } else { - log.Printf("error asserting tier to map[string]interface{}: %v", tier) - } - } - } else { - log.Printf("error asserting subscription_tiers to []interface{}: %v", rawTiers) - } + if err := viper.UnmarshalKey("subscription_tiers", &subscriptionTiers); err != nil { + log.Printf("Error unmarshaling subscription tiers: %v", err) + } else { + log.Println("Successfully fetched subscription tiers:", subscriptionTiers) } // Get relay info From a1d864c8cd456418a29a578221e99cf1decf513d Mon Sep 17 00:00:00 2001 From: Maphikza Date: Sat, 16 Nov 2024 15:06:56 +0200 Subject: [PATCH 21/21] feat: add GetSubscriberByAddress method to GORM store Adds functionality to retrieve subscriber information by Bitcoin address. This method supports transaction processing by allowing lookup of subscriber details when processing incoming Bitcoin payments. - Implements GetSubscriberByAddress in GormSubscriberStore - Returns complete subscriber record including npub for payment processing - Handles both 'not found' and general database error cases --- lib/stores/graviton/graviton.go | 16 ---------------- lib/stores/subscriber_store.go | 1 + .../subscription_store_gorm.go | 15 +++++++++++++++ lib/web/handler_update_wallet_transactions.go | 6 +++--- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/stores/graviton/graviton.go b/lib/stores/graviton/graviton.go index e9c1c60..ba646e8 100644 --- a/lib/stores/graviton/graviton.go +++ b/lib/stores/graviton/graviton.go @@ -1082,22 +1082,6 @@ func (store *GravitonStore) SaveSubscriber(subscriber *types.Subscriber) error { func (store *GravitonStore) GetSubscriberByAddress(address string) (*types.Subscriber, error) { - // Try GORM store first - if store.SubscriberStore != nil { - if subscriberStore, ok := store.SubscriberStore.(interface { - GetSubscriberByAddress(address string) (*types.Subscriber, error) - }); ok { - subscriber, err := subscriberStore.GetSubscriberByAddress(address) - if err == nil { - return subscriber, nil - } - // Only log the error if it's not a "not found" error - if !strings.Contains(err.Error(), "not found") { - log.Printf("GORM lookup failed: %v, falling back to Graviton", err) - } - } - } - store.mu.Lock() defer store.mu.Unlock() diff --git a/lib/stores/subscriber_store.go b/lib/stores/subscriber_store.go index 82d3a55..04777ae 100644 --- a/lib/stores/subscriber_store.go +++ b/lib/stores/subscriber_store.go @@ -14,4 +14,5 @@ type SubscriberStore interface { AddressExists(address string) (bool, error) SaveSubscriberAddress(address *lib.SubscriberAddress) error CountAvailableAddresses() (int64, error) + GetSubscriberByAddress(address string) (*lib.SubscriberAddress, error) } diff --git a/lib/stores/subscription_store/subscription_store_gorm.go b/lib/stores/subscription_store/subscription_store_gorm.go index bfe8d3b..5784e62 100644 --- a/lib/stores/subscription_store/subscription_store_gorm.go +++ b/lib/stores/subscription_store/subscription_store_gorm.go @@ -152,3 +152,18 @@ func (store *GormSubscriberStore) CountAvailableAddresses() (int64, error) { return count, nil } + +// GetSubscriberByAddress retrieves subscriber information by Bitcoin address +func (store *GormSubscriberStore) GetSubscriberByAddress(address string) (*types.SubscriberAddress, error) { + var subscriber types.SubscriberAddress + + err := store.DB.Where("address = ?", address).First(&subscriber).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("no subscriber found for address: %s", address) + } + return nil, fmt.Errorf("failed to query subscriber: %v", err) + } + + return &subscriber, nil +} diff --git a/lib/web/handler_update_wallet_transactions.go b/lib/web/handler_update_wallet_transactions.go index 6f530a3..ac5e23e 100644 --- a/lib/web/handler_update_wallet_transactions.go +++ b/lib/web/handler_update_wallet_transactions.go @@ -107,7 +107,7 @@ func processTransaction(store stores.Store, subManager *subscription.Subscriptio } // After subscriber retrieval in processTransaction - subscriber, err := store.GetSubscriberByAddress(txDetails.output) + subscriber, err := store.GetSubscriberStore().GetSubscriberByAddress(txDetails.output) if err != nil { log.Printf("Error: subscriber not found for address %s: %v", txDetails.output, err) return fmt.Errorf("subscriber not found for address %s: %v", txDetails.output, err) @@ -119,12 +119,12 @@ func processTransaction(store stores.Store, subManager *subscription.Subscriptio satoshis := int64(txDetails.value * 100_000_000) // Process the subscription payment - if err := subManager.ProcessPayment(subscriber.Npub, txID, satoshis); err != nil { + if err := subManager.ProcessPayment(*subscriber.Npub, txID, satoshis); err != nil { return fmt.Errorf("failed to process subscription: %v", err) } log.Printf("Successfully processed subscription payment for %s: %s sats", - subscriber.Npub, txDetails.valueStr) + *subscriber.Npub, txDetails.valueStr) return nil }