224 lines
6.5 KiB
Go
224 lines
6.5 KiB
Go
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,
|
|
})
|
|
}
|