package database import ( "fmt" "log" "pool-stats/helpers" "pool-stats/models" "sort" "time" "github.com/ostafen/clover/v2" "github.com/ostafen/clover/v2/document" c "github.com/ostafen/clover/v2/query" badgerstore "github.com/ostafen/clover/v2/store/badger" ) const ( CollectionName = "shares" TopSharesCollectionName = "TopShares" TimeWindowHighShareCollectionName = "TimeWindowHighShareStat" DailyStatsCollectionName = "DailyStats" ) func InitDatabase(path string) (*clover.DB, error) { store, err := badgerstore.Open(path) if err != nil { return nil, fmt.Errorf("failed to open BadgerDB store: %v", err) } db, err := clover.OpenWithStore(store) if err != nil { return nil, fmt.Errorf("failed to open CloverDB: %v", err) } // Ensure collection exists hasCollection, err := db.HasCollection(CollectionName) if err != nil { return nil, fmt.Errorf("failed to check collection: %v", err) } if !hasCollection { if err := db.CreateCollection(CollectionName); err != nil { return nil, fmt.Errorf("failed to create collection: %v", err) } if err := db.CreateIndex(CollectionName, "CreateDate"); err != nil { return nil, fmt.Errorf("failed to create index: %v", err) } } // Init TopShares collection hasTopSharesCollection, err := db.HasCollection(TopSharesCollectionName) if err != nil { return nil, fmt.Errorf("failed to check TopShares collection: %v", err) } if !hasTopSharesCollection { if err := db.CreateCollection(TopSharesCollectionName); err != nil { return nil, fmt.Errorf("failed to create TopShares collection: %v", err) } if err := db.CreateIndex(TopSharesCollectionName, "CreateDate"); err != nil { return nil, fmt.Errorf("failed to create index for TopShares: %v", err) } if err := db.CreateIndex(TopSharesCollectionName, "SDiff"); err != nil { return nil, fmt.Errorf("failed to create index for TopShares SDiff: %v", err) } } // Init TimeWindowHighShareStat collection hasTimeWindowCollection, err := db.HasCollection(TimeWindowHighShareCollectionName) if err != nil { return nil, fmt.Errorf("failed to check TimeWindowHighShare collection: %v", err) } if !hasTimeWindowCollection { if err := db.CreateCollection(TimeWindowHighShareCollectionName); err != nil { return nil, fmt.Errorf("failed to create TimeWindowHighShare collection: %v", err) } if err := db.CreateIndex(TimeWindowHighShareCollectionName, "TimeWindowID"); err != nil { return nil, fmt.Errorf("failed to create index for TimeWindowHighShare: %v", err) } } // Init DailyStats collection hasDailyStatsCollection, err := db.HasCollection(DailyStatsCollectionName) if err != nil { return nil, fmt.Errorf("failed to check DailyStats collection: %v", err) } if !hasDailyStatsCollection { if err := db.CreateCollection(DailyStatsCollectionName); err != nil { return nil, fmt.Errorf("failed to create DailyStats collection: %v", err) } if err := db.CreateIndex(DailyStatsCollectionName, "Date"); err != nil { return nil, fmt.Errorf("failed to create index for DailyStats: %v", err) } } return db, nil } func GetHighestSharesInRange(db *clover.DB, collection string, since time.Time, count int) ([]models.ShareLog, error) { // Convert `since` to the format in `createdate` lower := since.Unix() upper := time.Now().Unix() // Filter by timestamp range criteria := c.Field("CreateDate").GtEq(fmt.Sprint(lower)). And(c.Field("CreateDate").LtEq(fmt.Sprint(upper))) // Query sorted by "sdiff" descending, limit 1 results, err := db.FindAll(c.NewQuery(collection). Where(criteria). Sort(c.SortOption{Field: "SDiff", Direction: -1}). Limit(count)) if err != nil || len(results) == 0 { return nil, err } var shares []models.ShareLog for _, doc := range results { var s models.ShareLog if err := doc.Unmarshal(&s); err != nil { return nil, err } shares = append(shares, s) } return shares, nil } func PrintAllHashes(db *clover.DB) { docs, err := db.FindAll(c.NewQuery(CollectionName)) if err != nil { log.Fatalf("Failed to read from collection: %v", err) } for _, doc := range docs { hash := doc.Get("Hash") fmt.Println(hash) } } func ListShares(db *clover.DB, offset int, count int) []models.ShareLog { results, err := db.FindAll( c.NewQuery(CollectionName). Sort(c.SortOption{Field: "CreateDate", Direction: -1}). Skip(offset). Limit(count), ) if err != nil { log.Printf("failed to list shares: %v", err) return nil } shareLogs := make([]models.ShareLog, len(results)) for idx, doc := range results { var shareLog models.ShareLog doc.Unmarshal(&shareLog) shareLogs[idx] = shareLog } return shareLogs } func ListTopShares(db *clover.DB) []models.ShareLog { results, err := db.FindAll( c.NewQuery(TopSharesCollectionName). Sort(c.SortOption{Field: "SDiff", Direction: -1}), ) if err != nil { log.Printf("failed to list top shares: %v", err) return nil } topShares := make([]models.ShareLog, len(results)) for idx, doc := range results { var shareLog models.ShareLog doc.Unmarshal(&shareLog) topShares[idx] = shareLog } return topShares } func ReplaceTopShares(db *clover.DB, shares []models.ShareLog) { db.Delete(c.NewQuery(TopSharesCollectionName)) for _, share := range shares { doc := document.NewDocumentOf(&share) if _, err := db.InsertOne(TopSharesCollectionName, doc); err != nil { return } } } func GetTimeWindowHighShares(db *clover.DB) []models.TimeWindowHighShare { results, err := db.FindAll( c.NewQuery(TimeWindowHighShareCollectionName). Sort(c.SortOption{Field: "TimeWindowID", Direction: 1}), ) if err != nil { log.Printf("failed to list time window high shares: %v", err) return nil } timeWindowHighShares := make([]models.TimeWindowHighShare, len(results)) for idx, doc := range results { var timeWindowHighShare models.TimeWindowHighShare doc.Unmarshal(&timeWindowHighShare) timeWindowHighShares[idx] = timeWindowHighShare } return timeWindowHighShares } func SetTimeWindowHighShare(db *clover.DB, share models.TimeWindowHighShare) error { db.Delete( c.NewQuery(TimeWindowHighShareCollectionName). Where(c.Field("TimeWindowID"). Eq(share.TimeWindowID))) doc := document.NewDocumentOf(&share) db.InsertOne(TimeWindowHighShareCollectionName, doc) return nil } func ListSharesInTimeRange(db *clover.DB, since time.Time, till time.Time) []models.ShareLog { lower := since.Unix() upper := till.Unix() results, err := db.FindAll(c.NewQuery(CollectionName). Where(c.Field("CreateDate").GtEq(fmt.Sprint(lower)). And(c.Field("CreateDate").LtEq(fmt.Sprint(upper)))). Sort(c.SortOption{Field: "CreateDate", Direction: -1})) if err != nil { log.Printf("failed to list shares in time range: %v", err) return nil } shareLogs := make([]models.ShareLog, len(results)) for idx, doc := range results { var shareLog models.ShareLog doc.Unmarshal(&shareLog) shareLogs[idx] = shareLog } return shareLogs } // GetStatsForDay retrieves daily statistics for a given date // Tries to find from DailyStats collection, calculates on the fly if not found and stores func GetDailyStats(db *clover.DB, date time.Time) (*models.DailyStats, error) { dateStr := date.Format(time.DateOnly) // Check if stats already exist existingDoc, err := db.FindFirst(c.NewQuery(DailyStatsCollectionName). Where(c.Field("Date").Eq(dateStr))) if err == nil && existingDoc != nil { if existingDoc.ExpiresAt().After(time.Now()) { DeleteDailyStatsForDay(db, date) } else { var stats models.DailyStats if err := existingDoc.Unmarshal(&stats); err != nil { return nil, fmt.Errorf("failed to unmarshal daily stats: %v", err) } return &stats, nil } } // Get shares in range since := date.Truncate(24 * time.Hour) till := since.Add(24 * time.Hour) shares := ListSharesInTimeRange(db, since, till) sort.Slice(shares, func(i, j int) bool { return shares[i].SDiff > shares[j].SDiff }) // Calculate daily stats stats := &models.DailyStats{ Date: dateStr, ShareCount: len(shares), Workers: make(map[string]models.WorkerDailyStats), } if len(shares) > 0 { stats.TopShare = shares[0] stats.PoolHashrate = helpers.CalculateAverageHashrate(shares) } // Calculate worker stats sharesByWorker := make(map[string][]models.ShareLog) for _, share := range shares { sharesByWorker[share.WorkerName] = append(sharesByWorker[share.WorkerName], share) } for workerName, workerShares := range sharesByWorker { workerHashrate := helpers.CalculateAverageHashrate(workerShares) sort.Slice(workerShares, func(i, j int) bool { return workerShares[i].SDiff > workerShares[j].SDiff }) workerTopShare := workerShares[0] // Already sorted by SDiff stats.Workers[workerName] = models.WorkerDailyStats{ TopShare: workerTopShare, Hashrate: workerHashrate, Shares: len(workerShares), } } // Insert or update the daily stats in the collection doc := document.NewDocumentOf(stats) isToday := dateStr == time.Now().UTC().Format(time.DateOnly) if isToday { doc.SetExpiresAt(time.Now().Add(5 * time.Minute)) } if _, err := db.InsertOne(DailyStatsCollectionName, doc); err != nil { return nil, fmt.Errorf("failed to insert daily stats: %v", err) } return stats, nil } func ClearDailyStats(db *clover.DB) error { // Delete all documents in DailyStats collection if err := db.Delete(c.NewQuery(DailyStatsCollectionName)); err != nil { return fmt.Errorf("failed to clear DailyStats collection: %v", err) } return nil } func DeleteDailyStatsForDay(db *clover.DB, date time.Time) error { dateStr := date.Format(time.DateOnly) // Delete the document for the specific date if err := db.Delete(c.NewQuery(DailyStatsCollectionName). Where(c.Field("Date").Eq(dateStr))); err != nil { return fmt.Errorf("failed to delete daily stats for %s: %v", dateStr, err) } return nil }