// Package bitcoind provides a minimal Bitcoin Core JSON-RPC client. // Only the methods kamado-api actually needs are implemented. package bitcoind import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "time" ) type RPC struct { URL string User string Password string HTTP *http.Client } func NewRPC(url, user, password string, timeout time.Duration) *RPC { return &RPC{ URL: url, User: user, Password: password, HTTP: &http.Client{Timeout: timeout}, } } type rpcRequest struct { JSONRPC string `json:"jsonrpc"` ID string `json:"id"` Method string `json:"method"` Params []any `json:"params"` } type rpcResponse struct { Result json.RawMessage `json:"result"` Error *rpcError `json:"error"` ID string `json:"id"` } type rpcError struct { Code int `json:"code"` Message string `json:"message"` } func (e *rpcError) Error() string { return fmt.Sprintf("bitcoind rpc error %d: %s", e.Code, e.Message) } // Call performs a single JSON-RPC request and unmarshals the result. func (c *RPC) Call(ctx context.Context, method string, params []any, out any) error { body, err := json.Marshal(rpcRequest{ JSONRPC: "1.0", ID: "kamado", Method: method, Params: params, }) if err != nil { return err } req, err := http.NewRequestWithContext(ctx, "POST", c.URL, bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.SetBasicAuth(c.User, c.Password) resp, err := c.HTTP.Do(req) if err != nil { return fmt.Errorf("bitcoind rpc: %w", err) } defer resp.Body.Close() raw, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("bitcoind rpc: read body: %w", err) } // bitcoind returns 500 on rpc errors but still with a valid JSON body. var rr rpcResponse if err := json.Unmarshal(raw, &rr); err != nil { return fmt.Errorf("bitcoind rpc: unmarshal (status %d): %w: %s", resp.StatusCode, err, string(raw)) } if rr.Error != nil { return rr.Error } if out != nil { return json.Unmarshal(rr.Result, out) } return nil } // ---- typed method wrappers ---- type BlockchainInfo struct { Chain string `json:"chain"` Blocks int64 `json:"blocks"` Headers int64 `json:"headers"` BestBlockHash string `json:"bestblockhash"` Difficulty float64 `json:"difficulty"` MedianTime int64 `json:"mediantime"` VerificationProgress float64 `json:"verificationprogress"` InitialBlockDownload bool `json:"initialblockdownload"` } func (c *RPC) GetBlockchainInfo(ctx context.Context) (*BlockchainInfo, error) { var out BlockchainInfo if err := c.Call(ctx, "getblockchaininfo", nil, &out); err != nil { return nil, err } return &out, nil } // GetBlockHash returns the block hash at the given height. func (c *RPC) GetBlockHash(ctx context.Context, height int64) (string, error) { var out string if err := c.Call(ctx, "getblockhash", []any{height}, &out); err != nil { return "", err } return out, nil } // NetworkHashPS returns the network hashrate at the given block height. // `blocks` is a window (default 120). Pass -1 to use the default. func (c *RPC) GetNetworkHashPS(ctx context.Context, blocks, height int) (float64, error) { var out float64 if err := c.Call(ctx, "getnetworkhashps", []any{blocks, height}, &out); err != nil { return 0, err } return out, nil }