Skip to content

Commit f53b0cf

Browse files
authored
feat: chaintracks enpoints (#690)
1 parent 55a2dd9 commit f53b0cf

File tree

14 files changed

+316
-63
lines changed

14 files changed

+316
-63
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ go.work.sum
3838
infra-config.yaml
3939
infra-config.*.yaml
4040
!infra-config.example.yaml
41+
chaintracks-config.yaml
42+
chaintracks-config.*.yaml
43+
!chaintracks-config.example.yaml
4144

4245
# SQLite databases
4346
storage.sqlite

cmd/chaintracks/main.go

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,55 @@
11
package main
22

33
import (
4+
"context"
5+
"fmt"
46
"log/slog"
7+
"os"
58

9+
"github.com/bsv-blockchain/go-wallet-toolbox/internal/config"
610
"github.com/bsv-blockchain/go-wallet-toolbox/pkg/defs"
711
"github.com/bsv-blockchain/go-wallet-toolbox/pkg/services/chaintracks"
812
)
913

14+
const (
15+
envPrefix = "CHAINTRACKS"
16+
configFile = "chaintracks-config.yaml"
17+
)
18+
1019
func main() {
11-
config := defs.DefaultChaintracksServerConfig() // TODO: Allow loading from file/env
12-
server, err := chaintracks.NewServer(slog.Default(), config)
20+
ctx := context.Background()
21+
loader := config.NewLoader(defs.DefaultChaintracksServerConfig, envPrefix)
22+
23+
// optionally load from config file if it exists
24+
_, err := os.Stat(configFile)
25+
if !os.IsNotExist(err) {
26+
err := loader.SetConfigFilePath(configFile)
27+
if err != nil {
28+
panic(fmt.Errorf("failed to set config file path: %w", err))
29+
}
30+
slog.Default().Info("loading config from file", "file", configFile)
31+
} else {
32+
slog.Default().Info("config file not found, proceeding with environment variables and defaults")
33+
}
34+
35+
cfg, err := loader.Load()
36+
if err != nil {
37+
panic(fmt.Errorf("failed to load config: %w", err))
38+
}
39+
40+
err = cfg.Validate()
41+
if err != nil {
42+
panic(fmt.Errorf("config validation failed: %w", err))
43+
}
44+
45+
logger := chaintracks.MakeLogger(cfg.Logging)
46+
47+
server, err := chaintracks.NewServer(logger, cfg)
1348
if err != nil {
1449
panic(err)
1550
}
1651

17-
if err := server.ListenAndServe(); err != nil {
52+
if err := server.ListenAndServe(ctx); err != nil {
1853
panic(err)
1954
}
2055
}

cmd/chaintracks_config_gen/main.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"log"
7+
8+
"github.com/bsv-blockchain/go-wallet-toolbox/internal/config"
9+
"github.com/bsv-blockchain/go-wallet-toolbox/pkg/defs"
10+
)
11+
12+
func main() {
13+
outputFile := flag.String("output-file", "chaintracks-config.yaml", "Output configuration file path")
14+
flag.StringVar(outputFile, "o", "chaintracks-config.yaml", "Output configuration file path (shorthand)")
15+
flag.Parse()
16+
17+
cfg := defs.DefaultChaintracksServerConfig()
18+
19+
err := config.ToYAMLFile(cfg, *outputFile)
20+
if err != nil {
21+
log.Fatalf("Error writing configuration: %v\n", err)
22+
}
23+
24+
fmt.Printf("Chaintracks configuration written to %s\n", *outputFile)
25+
}

pkg/defs/chaintracks.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,9 @@ func (c *CDNBulkIngestorConfig) Validate() error {
139139
// ChaintracksServerConfig holds the configuration for the Chaintracks HTTP server and its underlying service settings.
140140
type ChaintracksServerConfig struct {
141141
ChaintracksServiceConfig
142-
Port uint `mapstructure:"port"`
143-
RoutingPrefix string `mapstructure:"routing_prefix"`
142+
Port uint `mapstructure:"port"`
143+
RoutingPrefix string `mapstructure:"routing_prefix"`
144+
Logging LogConfig `mapstructure:"logging"`
144145
}
145146

146147
// Validate checks if the ChaintracksServerConfig fields contain valid values and returns an error if any are invalid.
@@ -149,6 +150,10 @@ func (c *ChaintracksServerConfig) Validate() error {
149150
return fmt.Errorf("invalid chaintracks service config: %w", err)
150151
}
151152

153+
if err := c.Logging.Validate(); err != nil {
154+
return fmt.Errorf("invalid logging config: %w", err)
155+
}
156+
152157
const maxPort = 65535
153158
if c.Port == 0 || c.Port > maxPort {
154159
return fmt.Errorf("invalid port: %d", c.Port)
@@ -164,5 +169,6 @@ func DefaultChaintracksServerConfig() ChaintracksServerConfig {
164169
return ChaintracksServerConfig{
165170
Port: 3011,
166171
ChaintracksServiceConfig: DefaultChaintracksServiceConfig(),
172+
Logging: DefaultLogConfig(),
167173
}
168174
}

pkg/defs/logging.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package defs
22

3+
import (
4+
"fmt"
5+
)
6+
37
// LogLevel represents different log levels which can be configured.
48
type LogLevel string
59

@@ -29,3 +33,32 @@ const (
2933
func ParseHandlerTypeStr(handlerType string) (LogHandler, error) {
3034
return parseEnumCaseInsensitive(handlerType, JSONHandler, TextHandler)
3135
}
36+
37+
// LogConfig is the configuration for the logging
38+
type LogConfig struct {
39+
Enabled bool `mapstructure:"enabled"`
40+
Level LogLevel `mapstructure:"level"`
41+
Handler LogHandler `mapstructure:"handler"`
42+
}
43+
44+
// Validate validates the HTTP configuration
45+
func (c *LogConfig) Validate() (err error) {
46+
if c.Level, err = ParseLogLevelStr(string(c.Level)); err != nil {
47+
return fmt.Errorf("invalid log level: %w", err)
48+
}
49+
50+
if c.Handler, err = ParseHandlerTypeStr(string(c.Handler)); err != nil {
51+
return fmt.Errorf("invalid log handler: %w", err)
52+
}
53+
54+
return nil
55+
}
56+
57+
// DefaultLogConfig returns a LogConfig with logging enabled, level set to info, and using the JSON handler for output.
58+
func DefaultLogConfig() LogConfig {
59+
return LogConfig{
60+
Enabled: true,
61+
Level: LogLevelInfo,
62+
Handler: JSONHandler,
63+
}
64+
}

pkg/infra/config.go

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ type Config struct {
1919
FeeModel defs.FeeModel `mapstructure:"fee_model"`
2020
DBConfig defs.Database `mapstructure:"db"`
2121
HTTPConfig HTTPConfig `mapstructure:"http"`
22-
Logging LogConfig `mapstructure:"logging"`
22+
Logging defs.LogConfig `mapstructure:"logging"`
2323
Commission defs.Commission `mapstructure:"commission"`
2424
Services defs.WalletServices `mapstructure:"wallet_services"`
2525
Monitor defs.Monitor `mapstructure:"monitor"`
@@ -53,13 +53,6 @@ func (c *HTTPConfig) Validate() error {
5353
return nil
5454
}
5555

56-
// LogConfig is the configuration for the logging
57-
type LogConfig struct {
58-
Enabled bool `mapstructure:"enabled"`
59-
Level defs.LogLevel `mapstructure:"level"`
60-
Handler defs.LogHandler `mapstructure:"handler"`
61-
}
62-
6356
// Defaults returns the default configuration
6457
func Defaults() Config {
6558
network := defs.NetworkMainnet
@@ -74,12 +67,8 @@ func Defaults() Config {
7467
Port: 8100,
7568
RequestPrice: 0,
7669
},
77-
FeeModel: defs.DefaultFeeModel(),
78-
Logging: LogConfig{
79-
Enabled: true,
80-
Level: defs.LogLevelInfo,
81-
Handler: defs.JSONHandler,
82-
},
70+
FeeModel: defs.DefaultFeeModel(),
71+
Logging: defs.DefaultLogConfig(),
8372
Commission: defs.DefaultCommission(),
8473
Services: defs.DefaultServicesConfig(network),
8574
Monitor: defs.DefaultMonitorConfig(),
@@ -153,19 +142,6 @@ func (c *DBConfig) Validate() (err error) {
153142
return nil
154143
}
155144

156-
// Validate validates the HTTP configuration
157-
func (c *LogConfig) Validate() (err error) {
158-
if c.Level, err = defs.ParseLogLevelStr(string(c.Level)); err != nil {
159-
return fmt.Errorf("invalid log level: %w", err)
160-
}
161-
162-
if c.Handler, err = defs.ParseHandlerTypeStr(string(c.Handler)); err != nil {
163-
return fmt.Errorf("invalid log handler: %w", err)
164-
}
165-
166-
return nil
167-
}
168-
169145
// ToYAMLFile writes the configuration to a YAML file
170146
func (c *Config) ToYAMLFile(filename string) error {
171147
err := config.ToYAMLFile(c, filename)

pkg/services/chaintracks/chaintracks_handler.go

Lines changed: 121 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"log/slog"
77
"net/http"
88

9-
"github.com/bsv-blockchain/go-wallet-toolbox/pkg/defs"
109
"github.com/bsv-blockchain/go-wallet-toolbox/pkg/internal/logging"
1110
servercommon "github.com/bsv-blockchain/go-wallet-toolbox/pkg/internal/server"
1211
"github.com/bsv-blockchain/go-wallet-toolbox/pkg/services/chaintracks/models"
@@ -16,28 +15,31 @@ import (
1615
// Handler implements the HTTP API endpoints for Chaintracks services, including routing, logging, and config access.
1716
// It embeds an HTTP multiplexer, logger, and validated service configuration for BSV network operations.
1817
type Handler struct {
19-
logger *slog.Logger
20-
mux *http.ServeMux
21-
config defs.ChaintracksServiceConfig
18+
logger *slog.Logger
19+
mux *http.ServeMux
20+
service *Service
2221
}
2322

2423
// NewHandler creates a new Handler with the provided logger and ChaintracksServiceConfig.
2524
// NewHandler validates the config and registers HTTP handlers for root, robots.txt, and getChain endpoints.
2625
// Returns an initialized Handler or an error if validation fails.
27-
func NewHandler(logger *slog.Logger, config defs.ChaintracksServiceConfig) (*Handler, error) {
28-
if err := config.Validate(); err != nil {
29-
return nil, fmt.Errorf("invalid chaintracks service config: %w", err)
30-
}
31-
26+
func NewHandler(logger *slog.Logger, service *Service) (*Handler, error) {
3227
handler := &Handler{
33-
logger: logging.Child(logger, "chaintracks_handler"),
34-
mux: http.NewServeMux(),
35-
config: config,
28+
logger: logging.Child(logger, "chaintracks_handler"),
29+
mux: http.NewServeMux(),
30+
service: service,
3631
}
3732

38-
handler.mux.HandleFunc("/robots.txt", handler.handleRobotsTxt)
39-
handler.mux.HandleFunc("/", handler.handleRoot)
40-
handler.mux.HandleFunc("/getChain", handler.handleGetChain)
33+
handler.mux.HandleFunc("GET /robots.txt", handler.handleRobotsTxt)
34+
handler.mux.HandleFunc("GET /", handler.handleRoot)
35+
handler.mux.HandleFunc("GET /getChain", handler.handleGetChain)
36+
handler.mux.HandleFunc("GET /getInfo", handler.handleGetInfo)
37+
handler.mux.HandleFunc("GET /getPresentHeight", handler.handlePresentHeight)
38+
handler.mux.HandleFunc("GET /findChainTipHashHex", handler.handleFindTipHashHex)
39+
handler.mux.HandleFunc("GET /findHeaderHexForHeight", handler.handleFindHeaderHexForHeight)
40+
41+
// FIXME: in TS the endpoint is named findChainTipHeaderHex but it returns full JSON, not the hex
42+
handler.mux.HandleFunc("GET /findChainTipHeaderHex", handler.handleFindChainTipHeader)
4143

4244
return handler, nil
4345
}
@@ -56,7 +58,7 @@ func (h *Handler) handleRobotsTxt(w http.ResponseWriter, r *http.Request) {
5658

5759
func (h *Handler) handleRoot(w http.ResponseWriter, r *http.Request) {
5860
w.Header().Set("Content-Type", "text/plain")
59-
if _, err := fmt.Fprintf(w, "Chaintracks %sNet Block Header Service", string(h.config.Chain)); err != nil {
61+
if _, err := fmt.Fprintf(w, "Chaintracks %sNet Block Header Service", string(h.service.GetChain())); err != nil {
6062
h.logger.Error("failed to write root response", slog.String("error", err.Error()))
6163
}
6264
}
@@ -65,7 +67,109 @@ func (h *Handler) handleGetChain(w http.ResponseWriter, r *http.Request) {
6567
w.Header().Set("Content-Type", "application/json")
6668

6769
response := models.ResponseFrame[string]{
68-
Value: to.Ptr(string(h.config.Chain)),
70+
Value: to.Ptr(string(h.service.GetChain())),
71+
Status: models.ResponseStatusSuccess,
72+
}
73+
74+
h.writeJSONResponse(w, http.StatusOK, response)
75+
}
76+
77+
func (h *Handler) handleGetInfo(w http.ResponseWriter, r *http.Request) {
78+
w.Header().Set("Content-Type", "application/json")
79+
80+
info, err := h.service.GetInfo(r.Context())
81+
if err != nil {
82+
h.logger.Error("failed to get info", slog.String("error", err.Error()))
83+
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
84+
return
85+
}
86+
87+
response := models.ResponseFrame[models.InfoResponse]{
88+
Value: info,
89+
Status: models.ResponseStatusSuccess,
90+
}
91+
92+
h.writeJSONResponse(w, http.StatusOK, response)
93+
}
94+
95+
func (h *Handler) handlePresentHeight(w http.ResponseWriter, r *http.Request) {
96+
w.Header().Set("Content-Type", "application/json")
97+
98+
height, err := h.service.GetPresentHeight(r.Context())
99+
if err != nil {
100+
h.logger.Error("failed to get present height", slog.String("error", err.Error()))
101+
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
102+
return
103+
}
104+
105+
response := models.ResponseFrame[uint]{
106+
Value: to.Ptr(height),
107+
Status: models.ResponseStatusSuccess,
108+
}
109+
110+
h.writeJSONResponse(w, http.StatusOK, response)
111+
}
112+
113+
func (h *Handler) handleFindChainTipHeader(w http.ResponseWriter, r *http.Request) {
114+
w.Header().Set("Content-Type", "application/json")
115+
116+
tipHeader, err := h.service.FindChainTipHeader(r.Context())
117+
if err != nil {
118+
h.logger.Error("failed to find chain tip header hex", slog.String("error", err.Error()))
119+
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
120+
return
121+
}
122+
123+
response := models.ResponseFrame[models.BlockHeader]{
124+
Value: liveBlockHeaderToBlockHeaderDTO(tipHeader),
125+
Status: models.ResponseStatusSuccess,
126+
}
127+
128+
h.writeJSONResponse(w, http.StatusOK, response)
129+
}
130+
131+
func (h *Handler) handleFindTipHashHex(w http.ResponseWriter, r *http.Request) {
132+
w.Header().Set("Content-Type", "application/json")
133+
134+
tipHash, err := h.service.FindChainTipHeader(r.Context())
135+
if err != nil {
136+
h.logger.Error("failed to find chain tip hash hex", slog.String("error", err.Error()))
137+
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
138+
return
139+
}
140+
141+
response := models.ResponseFrame[string]{
142+
Value: to.Ptr(tipHash.Hash),
143+
Status: models.ResponseStatusSuccess,
144+
}
145+
146+
h.writeJSONResponse(w, http.StatusOK, response)
147+
}
148+
149+
func (h *Handler) handleFindHeaderHexForHeight(w http.ResponseWriter, r *http.Request) {
150+
w.Header().Set("Content-Type", "application/json")
151+
152+
heightParam := r.URL.Query().Get("height")
153+
if heightParam == "" {
154+
http.Error(w, "Missing 'height' query parameter", http.StatusBadRequest)
155+
return
156+
}
157+
158+
var height uint
159+
if _, err := fmt.Sscanf(heightParam, "%d", &height); err != nil {
160+
http.Error(w, "Invalid 'height' query parameter", http.StatusBadRequest)
161+
return
162+
}
163+
164+
header, err := h.service.FindHeaderForHeight(r.Context(), height)
165+
if err != nil {
166+
h.logger.Error("failed to find header hex for height", slog.String("error", err.Error()))
167+
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
168+
return
169+
}
170+
171+
response := models.ResponseFrame[models.BlockHeader]{
172+
Value: liveBlockHeaderToBlockHeaderDTO(header),
69173
Status: models.ResponseStatusSuccess,
70174
}
71175

0 commit comments

Comments
 (0)