// Package config loads kamado-api configuration from environment variables. // Missing required values are a hard error at startup; optional values get // documented defaults. package config import ( "fmt" "os" "strconv" "time" ) type Config struct { // HTTP server ListenAddr string // CKPool socket CKPoolSockDir string // Bitcoin Core RPC BitcoinRPCURL string // full URL e.g. http://bitcoind:8332 BitcoinRPCUser string BitcoinRPCPassword string BitcoinRPCTimeout time.Duration // ZMQ (Phase 2b) BitcoinZMQBlock string // e.g. tcp://bitcoind:28332, empty to disable // CKPool log file, for block-solve detection (Phase 2b) CKPoolLogFile string // SQLite DB path for persistence (Phase 2b) DBPath string // Poll interval for refreshing ckpool stats PollInterval time.Duration // Optional mempool.space-compatible explorer base URL. Empty means // the UI uses the public mempool.space; non-empty means the user // has pointed Kamado at their own instance via the StartOS config. MempoolBaseURL string // Directory ckpool's patched local_block_submit writes raw block // hex into. Empty disables the fallback submitter. PendingBlocksDir string // Comma- or newline-separated list of fallback bitcoind RPC URLs // that the submitter will try in order if a block stays pending // past the grace window. Each entry can encode credentials inline: // https://user:pass@host:8332/ // or without (e.g. an unauthenticated localhost backup node). BackupRPCURLs string // How long a pending block file must persist before the submitter // will try fallback RPCs. Defaults to 30s — long enough for // ckpool's own retry loop and a fast ckpool->bitcoind round-trip. PendingBlocksGrace time.Duration } func FromEnv() (*Config, error) { cfg := &Config{ ListenAddr: getenv("LISTEN_ADDR", ":8080"), CKPoolSockDir: getenv("CKPOOL_SOCKDIR", "/run/ckpool"), BitcoinRPCURL: os.Getenv("BITCOIN_RPC_URL"), BitcoinRPCUser: os.Getenv("BITCOIN_RPC_USER"), BitcoinRPCPassword: os.Getenv("BITCOIN_RPC_PASSWORD"), BitcoinRPCTimeout: getenvDuration("BITCOIN_RPC_TIMEOUT", 10*time.Second), BitcoinZMQBlock: os.Getenv("BITCOIN_ZMQ_BLOCK"), CKPoolLogFile: getenv("CKPOOL_LOGFILE", "/var/log/ckpool/ckpool.log"), DBPath: getenv("DB_PATH", "/var/lib/kamado/kamado.db"), PollInterval: getenvDuration("POLL_INTERVAL", 5*time.Second), MempoolBaseURL: os.Getenv("MEMPOOL_BASE_URL"), PendingBlocksDir: os.Getenv("PENDING_BLOCKS_DIR"), BackupRPCURLs: os.Getenv("BACKUP_RPC_URLS"), PendingBlocksGrace: getenvDuration("PENDING_BLOCKS_GRACE", 30*time.Second), } if cfg.BitcoinRPCURL == "" { return nil, fmt.Errorf("BITCOIN_RPC_URL is required") } if cfg.BitcoinRPCUser == "" || cfg.BitcoinRPCPassword == "" { return nil, fmt.Errorf("BITCOIN_RPC_USER and BITCOIN_RPC_PASSWORD are required") } return cfg, nil } func getenv(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def } func getenvDuration(key string, def time.Duration) time.Duration { v := os.Getenv(key) if v == "" { return def } // Accept both Go duration syntax ("5s", "10s") and bare seconds ("10"). if d, err := time.ParseDuration(v); err == nil { return d } if n, err := strconv.Atoi(v); err == nil { return time.Duration(n) * time.Second } return def }