package http import ( "context" "encoding/json" "errors" "net/http" "time" appconfig "github.com/D36u99er/bc-netts-energy/internal/config" "github.com/D36u99er/bc-netts-energy/internal/netts" "github.com/D36u99er/bc-netts-energy/internal/service" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "log/slog" ) // Server wraps the HTTP server. type Server struct { cfg appconfig.ServerConfig httpServer *http.Server energySvc *service.EnergyService logger *slog.Logger shutdownDur time.Duration } // NewServer constructs a Server with routes wired. func NewServer(cfg appconfig.ServerConfig, energySvc *service.EnergyService, logger *slog.Logger) *Server { router := chi.NewRouter() router.Use(middleware.RequestID) router.Use(middleware.RealIP) router.Use(middleware.Recoverer) s := &Server{ cfg: cfg, energySvc: energySvc, logger: logger, shutdownDur: cfg.ShutdownTimeout.Duration(), } router.Use(s.loggingMiddleware) router.Get("/healthz", s.handleHealth) router.Post("/api/v1/energy/rent", s.handleRentEnergy) httpServer := &http.Server{ Addr: cfg.Address, Handler: router, ReadTimeout: cfg.ReadTimeout.Duration(), WriteTimeout: cfg.WriteTimeout.Duration(), IdleTimeout: cfg.IdleTimeout.Duration(), } s.httpServer = httpServer return s } // Run starts the HTTP server and listens for shutdown from context. func (s *Server) Run(ctx context.Context) error { errCh := make(chan error, 1) go func() { s.logger.Info("http server listening", "address", s.cfg.Address) if err := s.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { errCh <- err } }() select { case <-ctx.Done(): return s.shutdown(context.Background()) case err := <-errCh: return err } } func (s *Server) shutdown(ctx context.Context) error { timeout := s.shutdownDur if timeout <= 0 { timeout = 15 * time.Second } ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() s.logger.Info("shutting down http server", "timeout", timeout) return s.httpServer.Shutdown(ctx) } func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{ "status": "ok", "time": time.Now().UTC().Format(time.RFC3339Nano), }) } func (s *Server) handleRentEnergy(w http.ResponseWriter, r *http.Request) { var req rentEnergyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request payload", err) return } defer r.Body.Close() ctx := r.Context() resp, err := s.energySvc.EnsureEnergy(ctx, service.EnsureEnergyRequest{ FromAddress: req.FromAddress, ToAddress: req.ToAddress, Amount: req.Amount, CallbackURL: req.CallbackURL, }) if err != nil { s.logger.ErrorContext(ctx, "ensure energy failed", "error", err) writeError(w, http.StatusBadGateway, "failed to ensure energy", err) return } writeJSON(w, http.StatusOK, newRentEnergyResponse(resp)) } func (s *Server) loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) next.ServeHTTP(ww, r) s.logger.Info("http request", "method", r.Method, "path", r.URL.Path, "status", ww.Status(), "duration", time.Since(start), "request_id", middleware.GetReqID(r.Context()), ) }) } type rentEnergyRequest struct { FromAddress string `json:"from_address"` ToAddress string `json:"to_address"` Amount string `json:"amount"` CallbackURL string `json:"callback_url"` } type rentEnergyResponse struct { FromAddress string `json:"from_address"` ToAddress string `json:"to_address"` Amount string `json:"amount"` AddressAdded bool `json:"address_added"` RecommendedEnergy int `json:"recommended_energy"` EnergyNeeded int `json:"energy_needed"` CyclesBefore int `json:"cycles_before"` CyclesAfter int `json:"cycles_after"` CyclesPurchased int `json:"cycles_purchased,omitempty"` TotalCycles int `json:"total_cycles,omitempty"` OrderID string `json:"order_id,omitempty"` OrderStatus string `json:"order_status,omitempty"` TotalCost float64 `json:"total_cost,omitempty"` NextDelegation int64 `json:"next_delegation_time,omitempty"` Analysis netts.TransferDetails `json:"analysis"` Status *statusSummary `json:"status,omitempty"` } type statusSummary struct { Mode string `json:"mode"` Status string `json:"status"` CyclesRemaining int `json:"cycles_remaining"` OpenOrders int `json:"open_orders"` ExpiryTime int64 `json:"expiry_time"` } func newRentEnergyResponse(resp *service.EnsureEnergyResponse) rentEnergyResponse { var status *statusSummary if resp.Status != nil { status = &statusSummary{ Mode: resp.Status.Mode, Status: resp.Status.Status, CyclesRemaining: resp.Status.CyclesRemaining, OpenOrders: resp.Status.OpenOrders, ExpiryTime: resp.Status.ExpiryTime, } } return rentEnergyResponse{ FromAddress: resp.FromAddress, ToAddress: resp.ToAddress, Amount: resp.Amount, AddressAdded: resp.AddressAdded, RecommendedEnergy: resp.RecommendedEnergy, EnergyNeeded: resp.EnergyNeeded, CyclesBefore: resp.CyclesBefore, CyclesAfter: resp.CyclesAfter, CyclesPurchased: resp.CyclesPurchased, TotalCycles: resp.TotalCycles, OrderID: resp.OrderID, OrderStatus: resp.OrderStatus, TotalCost: resp.TotalCost, NextDelegation: resp.NextDelegation, Analysis: resp.Analysis, Status: status, } } func writeJSON(w http.ResponseWriter, status int, body any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(body) } func writeError(w http.ResponseWriter, status int, message string, err error) { type errorBody struct { Error string `json:"error"` Details string `json:"details,omitempty"` } w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) detail := "" if err != nil { detail = err.Error() } _ = json.NewEncoder(w).Encode(errorBody{ Error: message, Details: detail, }) }