Go 1.22 module that polls CKPool's Unix socket control API, queries
Bitcoin Core over JSON-RPC, merges both into a thread-safe snapshot, and
serves it over REST. Layout:
api/
├── cmd/kamado-api/main.go signal-aware entrypoint
└── internal/
├── config/ env var loader with validation
├── ckpool/ socket client (4-byte LE length-prefixed wire
│ protocol verified against libckpool.c), typed
│ response models for poolstats/users/workers/
│ clients/uptime, + unit tests using a fake
│ unix socket server
├── bitcoind/ minimal JSON-RPC client, getblockchaininfo
│ and getnetworkhashps
├── state/ Aggregator that refreshes a merged Snapshot
│ on a ticker; readers get a copy under RWMutex
└── httpapi/ REST handlers on Go 1.22 ServeMux:
/api/health /api/pool /api/users
/api/workers /api/clients /api/snapshot
CKPool stores hashrate as "dsps" (diff shares per second); we convert
to H/s via the 2^32 constant used by GoBrrr-Pool and other clients.
Every stat CKPool exposes to its socket API is surfaced — useragent,
IP, per-client diff, per-worker best diff — closing the gap against
Bassin which only reads the 60-second stats files.
Dockerfile does a CGO_ENABLED=0 static build on golang:1.22-bookworm
with -trimpath -ldflags=-s -w. docker-compose now runs both ckpool and
kamado-api, sharing a named volume for /run/ckpool so the API can
dial the stratifier socket directly.
Deferred to Phase 2b (called out in README):
- Bitcoin Core ZMQ hashblock subscriber
- WebSocket push for real-time UI updates
- SQLite persistence (block history, best-share history)
- CKPool log tailer for "Solved and confirmed block" detection
Tests and build NOT run in this commit — Go isn't installed in the
dev environment. Run `make api-test` or `make api` to verify.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
72 lines
1.9 KiB
Go
72 lines
1.9 KiB
Go
// 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/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()
|
|
|
|
go agg.Run(ctx)
|
|
|
|
api := httpapi.New(agg, log)
|
|
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)
|
|
}
|
|
}
|