// kamado-api is the middleware that sits between ckpool-solo and the // Kamado dashboard. It polls ckpool's Unix socket API, calls bitcoind // over JSON-RPC, and serves the merged state over REST (WebSocket push, // SQLite persistence, ZMQ and log-tailer-based block detection land in // a follow-up commit). package main import ( "context" "errors" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "github.com/kamadopool/kamado-api/internal/bitcoind" "github.com/kamadopool/kamado-api/internal/ckpool" "github.com/kamadopool/kamado-api/internal/config" "github.com/kamadopool/kamado-api/internal/httpapi" "github.com/kamadopool/kamado-api/internal/logmon" "github.com/kamadopool/kamado-api/internal/state" ) func main() { log := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) slog.SetDefault(log) cfg, err := config.FromEnv() if err != nil { log.Error("config", "err", err) os.Exit(1) } log.Info("kamado-api starting", "listen", cfg.ListenAddr, "sockdir", cfg.CKPoolSockDir, "bitcoind", cfg.BitcoinRPCURL, "poll_interval", cfg.PollInterval, ) ck := ckpool.New(cfg.CKPoolSockDir) rpc := bitcoind.NewRPC(cfg.BitcoinRPCURL, cfg.BitcoinRPCUser, cfg.BitcoinRPCPassword, cfg.BitcoinRPCTimeout) agg := state.New(ck, rpc, cfg.PollInterval, log) ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() api := httpapi.New(agg, log) // Wire snapshot refreshes into the WebSocket hub so subscribers get // real-time updates without polling. agg.OnRefresh = api.Hub.Broadcast go agg.Run(ctx) // Tail the ckpool log for block-solve events. tailer := logmon.New(cfg.CKPoolLogFile, log) go tailer.Run(ctx) go agg.IngestBlockEvents(ctx, tailer.Events) srv := &http.Server{ Addr: cfg.ListenAddr, Handler: api.Handler(), ReadHeaderTimeout: 5 * time.Second, } // Shutdown on ctx cancel go func() { <-ctx.Done() log.Info("shutting down") shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() _ = srv.Shutdown(shutdownCtx) }() log.Info("http listening", "addr", cfg.ListenAddr) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Error("http server", "err", err) os.Exit(1) } }