diff --git a/.gitignore b/.gitignore index 7096a56..569f208 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +*.env # Dependency directories (remove the comment below to include it) # vendor/ diff --git a/go.mod b/go.mod index 7791b47..2d2c817 100644 --- a/go.mod +++ b/go.mod @@ -151,6 +151,7 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.1 // indirect github.com/ipfs/go-cid v0.4.1 + github.com/joho/godotenv v1.5.1 github.com/klauspost/compress v1.17.8 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/libp2p/go-libp2p-core v0.20.1 diff --git a/go.sum b/go.sum index aa0c363..4e848be 100644 --- a/go.sum +++ b/go.sum @@ -246,6 +246,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= diff --git a/lib/transports/websocket/auth.go b/lib/transports/websocket/auth.go index ef78e8a..ffb2ac3 100644 --- a/lib/transports/websocket/auth.go +++ b/lib/transports/websocket/auth.go @@ -5,6 +5,7 @@ import ( "github.com/nbd-wtf/go-nostr" lib_nostr "github.com/HORNET-Storage/hornet-storage/lib/handlers/nostr" + "github.com/HORNET-Storage/hornet-storage/lib/sessions" ) func handleAuthMessage(c *websocket.Conn, env *nostr.AuthEnvelope, challenge string, state *connectionState) { @@ -57,6 +58,12 @@ func handleAuthMessage(c *websocket.Conn, env *nostr.AuthEnvelope, challenge str return } + err = sessions.CreateSession(env.Event.PubKey) + if err != nil { + write("NOTICE", "Failed to create session") + return + } + err = AuthenticateConnection(c) if err != nil { write("OK", env.Event.ID, false, "Error authorizing connection") @@ -65,5 +72,12 @@ func handleAuthMessage(c *websocket.Conn, env *nostr.AuthEnvelope, challenge str state.authenticated = true + if state.authenticated { + // Authenticating user session. + userSession := sessions.GetSession(env.Event.PubKey) + userSession.Signature = &env.Event.Sig + userSession.Authenticated = true + } + write("OK", env.Event.ID, true, "") } diff --git a/lib/web/bitcoin-rate-from-apis.go b/lib/web/bitcoin-rate-from-apis.go index 55d6fc3..89e17b6 100644 --- a/lib/web/bitcoin-rate-from-apis.go +++ b/lib/web/bitcoin-rate-from-apis.go @@ -24,24 +24,25 @@ type BinanceResponse struct { Price string `json:"price"` } +type MempoolResponse struct { + USD float64 `json:"USD"` +} + func fetchCoinGeckoPrice() (float64, error) { resp, err := http.Get("https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd") if err != nil { return 0, err } defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) if err != nil { return 0, err } - var result CoinGeckoResponse err = json.Unmarshal(body, &result) if err != nil { return 0, err } - return result.Bitcoin.USD, nil } @@ -51,32 +52,46 @@ func fetchBinancePrice() (float64, error) { return 0, err } defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) if err != nil { return 0, err } - var result BinanceResponse err = json.Unmarshal(body, &result) if err != nil { return 0, err } - price, err := strconv.ParseFloat(result.Price, 64) if err != nil { return 0, err } - return price, nil } +func fetchMempoolPrice() (float64, error) { + resp, err := http.Get("https://mempool.space/api/v1/prices") + if err != nil { + return 0, err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return 0, err + } + var result MempoolResponse + err = json.Unmarshal(body, &result) + if err != nil { + return 0, err + } + return result.USD, nil +} + func fetchBitcoinPrice(apiIndex int) (float64, int, error) { apis := []func() (float64, error){ fetchCoinGeckoPrice, fetchBinancePrice, + fetchMempoolPrice, } - for i := 0; i < len(apis); i++ { index := (apiIndex + i) % len(apis) price, err := apis[index]() @@ -85,7 +100,6 @@ func fetchBitcoinPrice(apiIndex int) (float64, int, error) { } fmt.Println("Error fetching price from API", index, ":", err) } - return 0, apiIndex, fmt.Errorf("all API calls failed") } diff --git a/lib/web/get-wallet-addresses.go b/lib/web/get-wallet-addresses.go index c19ae18..c2082be 100644 --- a/lib/web/get-wallet-addresses.go +++ b/lib/web/get-wallet-addresses.go @@ -3,8 +3,8 @@ package web import ( "log" - types "github.com/HORNET-Storage/hornet-storage/lib" // Adjust the import path to your actual project structure - "github.com/HORNET-Storage/hornet-storage/lib/stores/graviton" // Adjust the import path to your actual project structure + types "github.com/HORNET-Storage/hornet-storage/lib" + "github.com/HORNET-Storage/hornet-storage/lib/stores/graviton" "github.com/gofiber/fiber/v2" "gorm.io/gorm" ) diff --git a/lib/web/get-wallet-transactions.go b/lib/web/get-wallet-transactions.go index 30c650b..61012a3 100644 --- a/lib/web/get-wallet-transactions.go +++ b/lib/web/get-wallet-transactions.go @@ -21,7 +21,7 @@ func handleLatestTransactions(c *fiber.Ctx) error { // Get the latest 10 transactions var transactions []types.WalletTransactions - result := db.Order("date desc").Limit(10).Find(&transactions) + result := db.Order("date desc").Limit(-1).Find(&transactions) if result.Error != nil { log.Printf("Error querying transactions: %v", result.Error) @@ -48,14 +48,14 @@ func handleLatestTransactions(c *fiber.Ctx) error { // Process each transaction to convert the value to USD for i, transaction := range transactions { - satoshis, err := strconv.ParseInt(transaction.Value, 10, 64) + value, err := strconv.ParseFloat(transaction.Value, 64) if err != nil { - log.Printf("Error converting value to int64: %v", err) + log.Printf("Error converting value to float64: %v", err) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": "Conversion error", }) } - transactions[i].Value = fmt.Sprintf("%.2f", satoshiToUSD(bitcoinRate.Rate, satoshis)) + transactions[i].Value = fmt.Sprintf("%.8f", value) } // Respond with the transactions diff --git a/lib/web/update-wallet-transactions.go b/lib/web/update-wallet-transactions.go index d019336..fd0f309 100644 --- a/lib/web/update-wallet-transactions.go +++ b/lib/web/update-wallet-transactions.go @@ -1,7 +1,9 @@ package web import ( + "fmt" "log" + "strconv" "time" types "github.com/HORNET-Storage/hornet-storage/lib" @@ -42,7 +44,8 @@ func handleTransactions(c *fiber.Ctx) error { continue } - date, err := time.Parse("2006-01-02 15:04:05", dateStr) + // Correct format for ISO 8601 datetime string with timezone + date, err := time.Parse(time.RFC3339, dateStr) if err != nil { log.Printf("Error parsing date: %v", err) continue @@ -54,14 +57,20 @@ func handleTransactions(c *fiber.Ctx) error { continue } - value, ok := transaction["value"].(string) + valueStr, ok := transaction["value"].(string) if !ok { log.Printf("Invalid value format: %v", transaction["value"]) continue } + value, err := strconv.ParseFloat(valueStr, 64) + if err != nil { + log.Printf("Error parsing value to float64: %v", err) + continue + } + var existingTransaction types.WalletTransactions - result := db.Where("address = ? AND date = ? AND output = ? AND value = ?", address, date, output, value).First(&existingTransaction) + result := db.Where("address = ? AND date = ? AND output = ? AND value = ?", address, date, output, valueStr).First(&existingTransaction) if result.Error == nil { // Transaction already exists, skip it @@ -79,7 +88,7 @@ func handleTransactions(c *fiber.Ctx) error { Address: address, Date: date, Output: output, - Value: value, + Value: fmt.Sprintf("%.8f", value), } if err := db.Create(&newTransaction).Error; err != nil { log.Printf("Error saving new transaction: %v", err) diff --git a/services/server/port/createkind411.go b/services/server/port/createkind411.go new file mode 100644 index 0000000..9060a77 --- /dev/null +++ b/services/server/port/createkind411.go @@ -0,0 +1,230 @@ +package main + +import ( + "crypto/ed25519" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "log" + "os" + "time" + + "github.com/HORNET-Storage/hornet-storage/lib/signing" + "github.com/HORNET-Storage/hornet-storage/lib/stores" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/joho/godotenv" + "github.com/nbd-wtf/go-nostr" + "github.com/spf13/viper" +) + +const envFile = ".env" +const nostrPrivateKeyVar = "NOSTR_PRIVATE_KEY" + +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"` +} + +func createKind411Event(privateKey *secp256k1.PrivateKey, publicKey *secp256k1.PublicKey, store stores.Store) error { + // Retrieve existing kind 411 events + filter := nostr.Filter{ + Kinds: []int{411}, + } + existingEvents, err := store.QueryEvents(filter) + if err != nil { + return fmt.Errorf("error querying existing kind 411 events: %v", err) + } + + // Delete existing kind 411 events if any + if len(existingEvents) > 0 { + for _, oldEvent := range existingEvents { + if err := store.DeleteEvent(oldEvent.ID); err != nil { + return fmt.Errorf("error deleting old kind 411 event %s: %v", oldEvent.ID, err) + } + log.Printf("Deleted existing kind 411 event with ID: %s", oldEvent.ID) + } + } + + // 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"), + } + + // Convert relay info to JSON + content, err := json.Marshal(relayInfo) + if err != nil { + return fmt.Errorf("error marshaling relay info: %v", err) + } + + // Create the event + event, err := createAnyEvent(privateKey, publicKey, 411, string(content), nil) + if err != nil { + return fmt.Errorf("error creating kind 411 event: %v", err) + } + + // Store the new event + if err := store.StoreEvent(event); err != nil { + return fmt.Errorf("error storing kind 411 event: %v", err) + } + + // Print the event for verification + eventJSON, err := json.MarshalIndent(event, "", " ") + if err != nil { + log.Printf("Error marshaling event for printing: %v", err) + } else { + log.Printf("Created and stored kind 411 event:\n%s", string(eventJSON)) + } + + log.Println("Kind 411 event created and stored successfully") + return nil +} + +func createAnyEvent(privateKey *secp256k1.PrivateKey, publicKey *secp256k1.PublicKey, kind int, content string, tags []nostr.Tag) (*nostr.Event, error) { + stringKey := hex.EncodeToString(schnorr.SerializePubKey(publicKey)) + log.Println("Public Key: ", stringKey) + + event := &nostr.Event{ + PubKey: stringKey, + CreatedAt: nostr.Timestamp(time.Now().Unix()), + Kind: kind, + Tags: tags, + Content: content, + } + + serializedEvent := serializeEventForID(event) + hash := sha256.Sum256([]byte(serializedEvent)) + eventID := hex.EncodeToString(hash[:]) + event.ID = eventID + + signature, err := schnorr.Sign(privateKey, hash[:]) + if err != nil { + return nil, err + } + + sigHex := hex.EncodeToString(signature.Serialize()) + event.Sig = sigHex + + err = signing.VerifySignature(signature, hash[:], publicKey) + if err != nil { + log.Printf("error verifying signature, %s", err) + return nil, fmt.Errorf("error verifying signature, %s", err) + } else { + log.Println("Signature is valid.") + } + + return event, nil +} + +func serializeEventForID(event *nostr.Event) string { + // Assuming the Tags and other fields are already correctly filled except ID and Sig + serialized, err := json.Marshal([]interface{}{ + 0, + event.PubKey, + event.CreatedAt, + event.Kind, + event.Tags, + event.Content, + }) + if err != nil { + panic(err) // Handle error properly in real code + } + + // Convert the JSON array to a string + compactSerialized := string(serialized) + + return compactSerialized +} + +func loadSecp256k1Keys() (*btcec.PrivateKey, *btcec.PublicKey, error) { + + privateKey, publicKey, err := signing.DeserializePrivateKey(os.Getenv("NOSTR_PRIVATE_KEY")) + if err != nil { + return nil, nil, fmt.Errorf("error getting keys: %s", err) + } + + return privateKey, publicKey, nil +} + +func generateEd25519Keypair(privateKeyHex string) (ed25519.PrivateKey, ed25519.PublicKey, error) { + // Convert hex string to byte slice + privateKeyBytes, err := hex.DecodeString(privateKeyHex) + if err != nil { + return nil, nil, fmt.Errorf("failed to decode hex string: %v", err) + } + + // Ensure the private key is the correct length + if len(privateKeyBytes) != ed25519.SeedSize { + return nil, nil, fmt.Errorf("invalid private key length: expected %d, got %d", ed25519.SeedSize, len(privateKeyBytes)) + } + + // Clamp the private key for Ed25519 usage + privateKeyBytes[0] &= 248 // Clear the lowest 3 bits + privateKeyBytes[31] &= 127 // Clear the highest bit + privateKeyBytes[31] |= 64 // Set the second highest bit + + // Generate the keypair + privateKey := ed25519.NewKeyFromSeed(privateKeyBytes) + publicKey := privateKey.Public().(ed25519.PublicKey) + + viper.Set("RelayDHTkey", publicKey) + log.Println("dht private key", hex.EncodeToString(privateKey)) + log.Println("dht public key", hex.EncodeToString(publicKey)) + viper.Set("RelayDHTkey", hex.EncodeToString(publicKey)) + + return privateKey, publicKey, nil +} + +func generateAndSaveNostrPrivateKey() error { + // Check if .env file exists and if NOSTR_PRIVATE_KEY is already set + if _, err := os.Stat(envFile); err == nil { + err := godotenv.Load(envFile) + if err != nil { + return fmt.Errorf("error loading .env file: %w", err) + } + + if os.Getenv(nostrPrivateKeyVar) != "" { + fmt.Println("NOSTR_PRIVATE_KEY is already set in .env file") + return nil + } + } + + // Generate new private key + privateKey, err := btcec.NewPrivateKey() + if err != nil { + return fmt.Errorf("error generating private key: %w", err) + } + + // Serialize and encode the private key + serializedPrivKey := hex.EncodeToString(privateKey.Serialize()) + + // Open .env file in append mode, create if not exists + f, err := os.OpenFile(envFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("error opening .env file: %w", err) + } + defer f.Close() + + // Write the new key to the file + if _, err := f.WriteString(fmt.Sprintf("\n%s=%s\n", nostrPrivateKeyVar, serializedPrivKey)); err != nil { + return fmt.Errorf("error writing to .env file: %w", err) + } + + fmt.Println("NOSTR_PRIVATE_KEY has been generated and saved to .env file") + return nil +} diff --git a/services/server/port/main.go b/services/server/port/main.go index bdf7296..ed16c65 100644 --- a/services/server/port/main.go +++ b/services/server/port/main.go @@ -12,6 +12,7 @@ import ( "syscall" merkle_dag "github.com/HORNET-Storage/scionic-merkletree/dag" + "github.com/joho/godotenv" "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/fsnotify/fsnotify" @@ -71,6 +72,13 @@ func init() { viper.SetDefault("relay_stats_db", "relay_stats.db") viper.SetDefault("query_cache", map[string]string{}) viper.SetDefault("service_tag", "hornet-storage-service") + viper.SetDefault("RelayName", "HORNETS") + viper.SetDefault("RelayDescription", "The best relay ever.") + viper.SetDefault("RelayPubkey", "") + viper.SetDefault("RelayContact", "support@hornets.net") + viper.SetDefault("RelaySoftware", "golang") + viper.SetDefault("RelayVersion", "0.0.1") + viper.SetDefault("RelayDHTkey", "") viper.AddConfigPath(".") viper.SetConfigType("json") @@ -104,6 +112,37 @@ func main() { queryCache := viper.GetStringMapString("query_cache") store.InitStore(queryCache) + // generate server priv key if it does not exist + err := generateAndSaveNostrPrivateKey() + if err != nil { + log.Printf("error generating or saving server private key") + } + + err = godotenv.Load(envFile) + if err != nil { + log.Printf("error loading .env file: %s", err) + return + } + + // load keys from environment for signing kind 411 + privKey, pubKey, err := loadSecp256k1Keys() + if err != nil { + log.Printf("error loading keys from environment. check if you have the key in the environment: %s", err) + return + } + // Create dht key for using relay private key and set it on viper. + _, _, err = generateEd25519Keypair(os.Getenv("NOSTR_PRIVATE_KEY")) + if err != nil { + log.Printf("error generating dht-key: %s", err) + return + } + + // Create and store kind 411 event + if err := createKind411Event(privKey, pubKey, store); err != nil { + log.Printf("Failed to create kind 411 event: %v", err) + return + } + // Stream Handlers download.AddDownloadHandler(host, store, func(rootLeaf *merkle_dag.DagLeaf, pubKey *string, signature *string) bool { return true