301 lines
7.8 KiB
Go
301 lines
7.8 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"io/fs"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"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 logsDir = "/home/pk/pro/pkstats/logs"
|
|
const collectionName = "shares"
|
|
|
|
type ShareLog struct {
|
|
WorkInfoID uint64 `json:"workinfoid"` // ID of the associated work unit
|
|
ClientID int `json:"clientid"` // ID of the client/miner connection
|
|
Enonce1 string `json:"enonce1"` // Extra nonce 1 (assigned by pool)
|
|
Nonce2 string `json:"nonce2"` // Extra nonce 2 (sent by miner)
|
|
Nonce string `json:"nonce"` // Nonce used to generate the solution
|
|
NTime string `json:"ntime"` // Network time used in the block
|
|
Diff float64 `json:"diff"` // Target difficulty for the share
|
|
SDiff float64 `json:"sdiff"` // Share difficulty achieved
|
|
Hash string `json:"hash"` // Resulting hash from the share
|
|
Result bool `json:"result"` // Was this share a valid result
|
|
Errn int `json:"errn"` // Error code (0 = no error)
|
|
|
|
CreateDate string `json:"createdate"` // Timestamp: "seconds,nanoseconds"
|
|
CreateBy string `json:"createby"` // Origin of share creation ("code")
|
|
CreateCode string `json:"createcode"` // Component that created this entry
|
|
CreateInet string `json:"createinet"` // IP + port of submit origin
|
|
|
|
WorkerName string `json:"workername"` // Full worker name (username.worker)
|
|
Username string `json:"username"` // User's Bitcoin address
|
|
Address string `json:"address"` // IP address of the worker
|
|
Agent string `json:"agent"` // Miner agent string (e.g., bitaxe/BM1370)
|
|
}
|
|
|
|
type ShareStat struct {
|
|
Label string
|
|
Diff string
|
|
Time string
|
|
}
|
|
|
|
type Handlers struct {
|
|
DB *clover.DB
|
|
}
|
|
|
|
// ParseCreateDate can be used to convert ShareLog.CreateDate to time.Time
|
|
func (s *ShareLog) ParseCreateDate() (time.Time, error) {
|
|
var sec, nsec int64
|
|
_, err := fmt.Sscanf(s.CreateDate, "%d,%d", &sec, &nsec)
|
|
if err != nil {
|
|
return time.Time{}, err
|
|
}
|
|
return time.Unix(sec, nsec), nil
|
|
}
|
|
|
|
func main() {
|
|
store, _ := badgerstore.Open("badgerdb")
|
|
db, err := clover.OpenWithStore(store)
|
|
if err != nil {
|
|
log.Fatalf("Failed to open CloverDB: %v", err)
|
|
}
|
|
|
|
// Ensure collection exists
|
|
hasCollection, err := db.HasCollection(collectionName)
|
|
if err != nil {
|
|
log.Fatalf("Failed to check collection: %v", err)
|
|
}
|
|
if !hasCollection {
|
|
_ = db.CreateCollection(collectionName)
|
|
db.CreateIndex(collectionName, "CreateDate")
|
|
}
|
|
|
|
printAllHashes(db)
|
|
lastDayHighestShare, err := getHighestShareInRange(db, collectionName, time.Now().Add(-24*time.Hour))
|
|
if err != nil {
|
|
log.Printf("Failed to get highest share in last 24h: %v", err)
|
|
} else {
|
|
createdAt, _ := lastDayHighestShare.Get("CreateDate").(string)
|
|
fmt.Printf("Highest share in last 24h: %s, Diff: %f, Created at: %s\n",
|
|
lastDayHighestShare.Get("Hash"),
|
|
lastDayHighestShare.Get("SDiff"),
|
|
createdAt,
|
|
)
|
|
}
|
|
|
|
go watchAndIngest(db)
|
|
|
|
go func() {
|
|
handlers := Handlers{DB: db}
|
|
http.HandleFunc("/", handlers.indexHandler)
|
|
fmt.Println("Listening on :8081")
|
|
log.Fatal(http.ListenAndServe(":8081", nil))
|
|
}()
|
|
|
|
fmt.Println("Waitin for ctrl-c")
|
|
sigs := make(chan os.Signal, 1)
|
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
|
<-sigs
|
|
store.Close()
|
|
}
|
|
|
|
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 watchAndIngest(db *clover.DB) {
|
|
ticker := time.NewTicker(30 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
<-ticker.C
|
|
ingestClosedBlocks(db)
|
|
}
|
|
}
|
|
|
|
func ingestClosedBlocks(db *clover.DB) {
|
|
entries, err := os.ReadDir(logsDir)
|
|
if err != nil {
|
|
log.Println("Error reading logsDir:", err)
|
|
return
|
|
}
|
|
|
|
// Find block-like directories (starts with 000)
|
|
blockDirs := []fs.DirEntry{}
|
|
for _, entry := range entries {
|
|
if entry.IsDir() && strings.HasPrefix(entry.Name(), "000") {
|
|
blockDirs = append(blockDirs, entry)
|
|
}
|
|
}
|
|
|
|
if len(blockDirs) <= 1 {
|
|
return // Nothing to ingest
|
|
}
|
|
|
|
// Sort dirs alphabetically (ascending), last is newest
|
|
sort.Slice(blockDirs, func(i, j int) bool {
|
|
return blockDirs[i].Name() < blockDirs[j].Name()
|
|
})
|
|
|
|
// Ingest all except last (current block dir)
|
|
for _, dir := range blockDirs[:len(blockDirs)-1] {
|
|
ingestBlockDir(db, filepath.Join(logsDir, dir.Name()))
|
|
_ = os.RemoveAll(filepath.Join(logsDir, dir.Name()))
|
|
}
|
|
}
|
|
|
|
func ingestBlockDir(db *clover.DB, dirPath string) {
|
|
files, err := os.ReadDir(dirPath)
|
|
if err != nil {
|
|
log.Printf("Failed to read block dir %s: %v", dirPath, err)
|
|
return
|
|
}
|
|
|
|
for _, f := range files {
|
|
if !strings.HasSuffix(f.Name(), ".sharelog") {
|
|
continue
|
|
}
|
|
|
|
data, err := os.ReadFile(filepath.Join(dirPath, f.Name()))
|
|
if err != nil {
|
|
log.Println("Failed to read file:", err)
|
|
continue
|
|
}
|
|
|
|
lines := strings.Split(string(data), "\n")
|
|
for _, line := range lines {
|
|
if strings.TrimSpace(line) == "" {
|
|
continue
|
|
}
|
|
|
|
var share ShareLog
|
|
if err := json.Unmarshal([]byte(line), &share); err != nil {
|
|
log.Println("JSON parse error:", err)
|
|
continue
|
|
}
|
|
|
|
doc := document.NewDocumentOf(&share)
|
|
if _, err := db.InsertOne(collectionName, doc); err != nil {
|
|
log.Println("DB insert error:", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
log.Printf("Ingested and deleted %s", dirPath)
|
|
}
|
|
|
|
func parseCreatedate(createdate string) time.Time {
|
|
parts := strings.Split(createdate, ",")
|
|
if len(parts) == 2 {
|
|
sec, _ := strconv.ParseInt(parts[0], 10, 64)
|
|
nsec, _ := strconv.ParseInt(parts[1], 10, 64)
|
|
return time.Unix(sec, nsec)
|
|
}
|
|
return time.Time{}
|
|
}
|
|
|
|
func getHighestShareInRange(db *clover.DB, collection string, since time.Time) (*document.Document, 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(1))
|
|
|
|
if err != nil || len(results) == 0 {
|
|
return nil, err
|
|
}
|
|
return results[0], nil
|
|
}
|
|
|
|
func (h Handlers) indexHandler(w http.ResponseWriter, r *http.Request) {
|
|
stats, err := getStats(h.DB)
|
|
if err != nil {
|
|
http.Error(w, "Failed to load stats", 500)
|
|
return
|
|
}
|
|
|
|
tmpl := template.Must(template.ParseFiles("templates/index.html"))
|
|
tmpl.Execute(w, stats)
|
|
}
|
|
|
|
func humanDiff(diff float64) string {
|
|
units := []string{"", "K", "M", "G", "T"}
|
|
i := 0
|
|
for diff >= 1000 && i < len(units)-1 {
|
|
diff /= 1000
|
|
i++
|
|
}
|
|
return fmt.Sprintf("%.2f%s", diff, units[i])
|
|
}
|
|
|
|
func getStats(db *clover.DB) ([]ShareStat, error) {
|
|
now := time.Now()
|
|
ranges := []struct {
|
|
Label string
|
|
Since time.Time
|
|
}{
|
|
{"Past Hour", now.Add(-1 * time.Hour)},
|
|
{"Past 24h", now.Add(-24 * time.Hour)},
|
|
{"Past 7d", now.Add(-7 * 24 * time.Hour)},
|
|
}
|
|
|
|
stats := []ShareStat{}
|
|
|
|
// All-time highest
|
|
doc, _ := getHighestShareInRange(db, collectionName, time.Unix(0, 0))
|
|
if doc != nil {
|
|
stats = append(stats, ShareStat{
|
|
Label: "All Time",
|
|
Diff: humanDiff(doc.Get("SDiff").(float64)),
|
|
Time: parseCreatedate(doc.Get("CreateDate").(string)).Format(time.RFC822),
|
|
})
|
|
}
|
|
|
|
for _, r := range ranges {
|
|
doc, _ := getHighestShareInRange(db, collectionName, r.Since)
|
|
if doc != nil {
|
|
stats = append(stats, ShareStat{
|
|
Label: r.Label,
|
|
Diff: humanDiff(doc.Get("SDiff").(float64)),
|
|
Time: parseCreatedate(doc.Get("CreateDate").(string)).Format(time.RFC822),
|
|
})
|
|
} else {
|
|
stats = append(stats, ShareStat{Label: r.Label, Diff: "-", Time: "-"})
|
|
}
|
|
}
|
|
|
|
return stats, nil
|
|
}
|