feat: initial Netts energy orchestrator
This commit is contained in:
2
internal/http/doc.go
Normal file
2
internal/http/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package http provides HTTP handlers for the energy service.
|
||||
package http
|
||||
223
internal/http/server.go
Normal file
223
internal/http/server.go
Normal file
@@ -0,0 +1,223 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user