// Package api provides HTTP API handlers package api import ( "encoding/json" "fmt" "net/http" "strings" "time" "spiderman/internal/fabric" "spiderman/internal/logger" "spiderman/pkg/models" "github.com/google/uuid" "github.com/gorilla/mux" "github.com/sirupsen/logrus" ) // Handler handles HTTP requests and communicates with Fabric type Handler struct { fabricClient *fabric.Client } // NewHandler creates a new API handler func NewHandler(fabricClient *fabric.Client) *Handler { logger.Logger.Info("API handler initialized") return &Handler{ fabricClient: fabricClient, } } // writeJSONResponse writes a JSON response to the HTTP response writer func writeJSONResponse(w http.ResponseWriter, statusCode int, response models.Response) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) json.NewEncoder(w).Encode(response) } // writeErrorResponse writes an error response to the HTTP response writer func writeErrorResponse(w http.ResponseWriter, statusCode int, message string) { writeJSONResponse(w, statusCode, models.Response{ Success: false, Message: message, }) } // HealthCheck godoc // // @Summary Health check // @Description Check the health status of the API // @Tags health // @Accept json // @Produce json // @Success 200 {object} models.HealthResponse // @Router /health [get] func (h *Handler) HealthCheck(w http.ResponseWriter, r *http.Request) { requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr) requestLogger.Debug("Processing health check request") w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(models.HealthResponse{Status: "healthy"}) requestLogger.Info("Health check completed successfully") } // InitLedger godoc // // @Summary Initialize ledger // @Description Initialize the blockchain ledger with a set of assets // @Tags ledger // @Accept json // @Produce json // @Success 200 {object} models.Response // @Failure 500 {object} models.ErrorResponse // @Router /ledger/init [post] func (h *Handler) InitLedger(w http.ResponseWriter, r *http.Request) { requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr) requestLogger.Info("Starting ledger initialization") start := time.Now() err := h.fabricClient.InitLedger() duration := time.Since(start) if err != nil { requestLogger.WithFields(logrus.Fields{ "error": err.Error(), "duration_ms": duration.Milliseconds(), }).Error("Failed to initialize ledger") writeErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("Failed to initialize ledger: %s", fabric.HandleFabricError(err))) return } requestLogger.WithFields(logrus.Fields{ "duration_ms": duration.Milliseconds(), }).Info("Ledger initialized successfully") writeJSONResponse(w, http.StatusOK, models.Response{ Success: true, Message: "Ledger initialized successfully", }) } // GetAllAssets godoc // // @Summary Get all assets // @Description Retrieve all assets from the blockchain ledger // @Tags assets // @Accept json // @Produce json // @Success 200 {object} models.Response{data=[]models.Asset} // @Failure 500 {object} models.ErrorResponse // @Router /assets [get] func (h *Handler) GetAllAssets(w http.ResponseWriter, r *http.Request) { requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr) requestLogger.Info("Retrieving all assets") start := time.Now() assets, err := h.fabricClient.GetAllAssets() duration := time.Since(start) if err != nil { requestLogger.WithFields(logrus.Fields{ "error": err.Error(), "duration_ms": duration.Milliseconds(), }).Error("Failed to retrieve assets") writeErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get assets: %s", fabric.HandleFabricError(err))) return } requestLogger.WithFields(logrus.Fields{ "asset_count": len(assets), "duration_ms": duration.Milliseconds(), }).Info("Assets retrieved successfully") writeJSONResponse(w, http.StatusOK, models.Response{ Success: true, Data: assets, }) } // CreateAsset godoc // // @Summary Create a new asset // @Description Create a new asset on the blockchain ledger with auto-generated UUID if ID not provided // @Tags assets // @Accept json // @Produce json // @Param asset body models.CreateAssetRequest true "Asset data (ID is optional and will be auto-generated)" // @Success 201 {object} models.Response{data=object{asset=models.Asset,transactionId=string}} // @Failure 400 {object} models.ErrorResponse // @Failure 500 {object} models.ErrorResponse // @Router /assets [post] func (h *Handler) CreateAsset(w http.ResponseWriter, r *http.Request) { requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr) requestLogger.Info("Creating new asset") var req models.CreateAssetRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { requestLogger.WithError(err).Warn("Invalid request body for asset creation") writeErrorResponse(w, http.StatusBadRequest, "Invalid request body") return } // Auto-generate UUID if ID is not provided if req.ID == "" { req.ID = uuid.New().String() requestLogger.WithField("generated_id", req.ID).Info("Auto-generated UUID for asset") } requestLogger.WithFields(logrus.Fields{ "asset_id": req.ID, "owner": req.Owner, "color": req.Color, "size": req.Size, "value": req.AppraisedValue, }).Debug("Asset creation request details") // Convert request to asset model asset := models.Asset{ ID: req.ID, Color: req.Color, Size: req.Size, Owner: req.Owner, AppraisedValue: req.AppraisedValue, } start := time.Now() transactionResult, err := h.fabricClient.CreateAsset(asset) duration := time.Since(start) if err != nil { requestLogger.WithFields(logrus.Fields{ "error": err.Error(), "asset_id": req.ID, "duration_ms": duration.Milliseconds(), }).Error("Failed to create asset") writeErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("Failed to create asset: %s", fabric.HandleFabricError(err))) return } requestLogger.WithFields(logrus.Fields{ "asset_id": req.ID, "transaction_result": transactionResult, "duration_ms": duration.Milliseconds(), }).Info("Asset created successfully") // Create response data that includes both asset and transaction ID responseData := map[string]interface{}{ "asset": asset, "transactionId": transactionResult, } writeJSONResponse(w, http.StatusCreated, models.Response{ Success: true, Message: "Asset created successfully", Data: responseData, }) } // GetAssetByID godoc // // @Summary Get asset by ID // @Description Retrieve a specific asset by its ID from the blockchain ledger // @Tags assets // @Accept json // @Produce json // @Param id path string true "Asset ID" // @Success 200 {object} models.Response{data=models.Asset} // @Failure 404 {object} models.ErrorResponse // @Failure 500 {object} models.ErrorResponse // @Router /assets/{id} [get] func (h *Handler) GetAssetByID(w http.ResponseWriter, r *http.Request) { requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr) vars := mux.Vars(r) assetID := vars["id"] requestLogger.WithField("asset_id", assetID).Info("Retrieving asset by ID") start := time.Now() asset, err := h.fabricClient.ReadAssetByID(assetID) duration := time.Since(start) if err != nil { requestLogger.WithFields(logrus.Fields{ "error": err.Error(), "asset_id": assetID, "duration_ms": duration.Milliseconds(), }).Error("Failed to retrieve asset") statusCode := http.StatusInternalServerError if err.Error() == "asset not found" { statusCode = http.StatusNotFound } writeErrorResponse(w, statusCode, fmt.Sprintf("Failed to get asset: %s", fabric.HandleFabricError(err))) return } requestLogger.WithFields(logrus.Fields{ "asset_id": assetID, "duration_ms": duration.Milliseconds(), }).Info("Asset retrieved successfully") writeJSONResponse(w, http.StatusOK, models.Response{ Success: true, Data: asset, }) } // TransferAsset godoc // // @Summary Transfer asset ownership // @Description Transfer ownership of an asset to a new owner // @Tags assets // @Accept json // @Produce json // @Param id path string true "Asset ID" // @Param request body models.TransferAssetRequest true "Transfer request" // @Success 200 {object} models.Response // @Failure 400 {object} models.ErrorResponse // @Failure 404 {object} models.ErrorResponse // @Failure 500 {object} models.ErrorResponse // @Router /assets/{id}/transfer [put] func (h *Handler) TransferAsset(w http.ResponseWriter, r *http.Request) { requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr) vars := mux.Vars(r) assetID := vars["id"] requestLogger.WithField("asset_id", assetID).Info("Transferring asset") var req models.TransferAssetRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { requestLogger.WithError(err).Warn("Invalid request body for asset transfer") writeErrorResponse(w, http.StatusBadRequest, "Invalid request body") return } requestLogger.WithFields(logrus.Fields{ "asset_id": assetID, "new_owner": req.NewOwner, }).Debug("Asset transfer request details") start := time.Now() err := h.fabricClient.TransferAsset(assetID, req.NewOwner) duration := time.Since(start) if err != nil { requestLogger.WithFields(logrus.Fields{ "error": err.Error(), "asset_id": assetID, "new_owner": req.NewOwner, "duration_ms": duration.Milliseconds(), }).Error("Failed to transfer asset") statusCode := http.StatusInternalServerError if err.Error() == "asset not found" { statusCode = http.StatusNotFound } writeErrorResponse(w, statusCode, fmt.Sprintf("Failed to transfer asset: %s", fabric.HandleFabricError(err))) return } requestLogger.WithFields(logrus.Fields{ "asset_id": assetID, "new_owner": req.NewOwner, "duration_ms": duration.Milliseconds(), }).Info("Asset transferred successfully") writeJSONResponse(w, http.StatusOK, models.Response{ Success: true, Message: "Asset transferred successfully", }) } // UpdateAsset godoc // // @Summary Update an existing asset // @Description Update an existing asset's information on the blockchain // @Tags assets // @Accept json // @Produce json // @Param id path string true "Asset ID" // @Param asset body models.CreateAssetRequest true "Updated asset data" // @Success 200 {object} models.Response{data=models.Asset} // @Failure 400 {object} models.ErrorResponse // @Failure 500 {object} models.ErrorResponse // @Router /assets/{id} [put] func (h *Handler) UpdateAsset(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) assetID := vars["id"] requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr). WithField("asset_id", assetID) requestLogger.Info("Updating asset") var req models.CreateAssetRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { requestLogger.WithError(err).Warn("Invalid request body for asset update") writeErrorResponse(w, http.StatusBadRequest, "Invalid request body") return } // Set the ID from the URL req.ID = assetID requestLogger.WithFields(logrus.Fields{ "owner": req.Owner, "color": req.Color, "size": req.Size, "value": req.AppraisedValue, }).Debug("Asset update request details") // Convert request to asset model asset := models.Asset{ ID: req.ID, Color: req.Color, Size: req.Size, Owner: req.Owner, AppraisedValue: req.AppraisedValue, } start := time.Now() err := h.fabricClient.UpdateAsset(asset) duration := time.Since(start) if err != nil { requestLogger.WithFields(logrus.Fields{ "error": err.Error(), "duration_ms": duration.Milliseconds(), }).Error("Failed to update asset") writeErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("Failed to update asset: %s", fabric.HandleFabricError(err))) return } requestLogger.WithFields(logrus.Fields{ "duration_ms": duration.Milliseconds(), }).Info("Asset updated successfully") writeJSONResponse(w, http.StatusOK, models.Response{ Success: true, Message: "Asset updated successfully", Data: asset, }) } // GetBlockHeight godoc // // @Summary Get block height // @Description Get the current block height of the blockchain // @Tags blockchain // @Accept json // @Produce json // @Success 200 {object} models.Response{data=models.BlockHeightResponse} // @Failure 500 {object} models.ErrorResponse // @Router /blockchain/height [get] func (h *Handler) GetBlockHeight(w http.ResponseWriter, r *http.Request) { requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr) requestLogger.Info("Getting block height") start := time.Now() height, err := h.fabricClient.GetBlockHeight() duration := time.Since(start) if err != nil { requestLogger.WithFields(logrus.Fields{ "error": err.Error(), "duration_ms": duration.Milliseconds(), }).Error("Failed to get block height") writeErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get block height: %s", fabric.HandleFabricError(err))) return } requestLogger.WithFields(logrus.Fields{ "height": height, "duration_ms": duration.Milliseconds(), }).Info("Block height retrieved successfully") response := models.BlockHeightResponse{ Height: height, } writeJSONResponse(w, http.StatusOK, models.Response{ Success: true, Message: "Block height retrieved successfully", Data: response, }) } // GetChainInfo godoc // // @Summary Get chain information // @Description Get detailed information about the blockchain including block height // @Tags blockchain // @Accept json // @Produce json // @Success 200 {object} models.Response{data=models.ChainInfoResponse} // @Failure 500 {object} models.ErrorResponse // @Router /blockchain/info [get] func (h *Handler) GetChainInfo(w http.ResponseWriter, r *http.Request) { requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr) requestLogger.Info("Getting chain information") start := time.Now() height, err := h.fabricClient.GetBlockHeight() duration := time.Since(start) if err != nil { requestLogger.WithFields(logrus.Fields{ "error": err.Error(), "duration_ms": duration.Milliseconds(), }).Error("Failed to get chain information") writeErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get chain information: %s", fabric.HandleFabricError(err))) return } requestLogger.WithFields(logrus.Fields{ "height": height, "duration_ms": duration.Milliseconds(), }).Info("Chain information retrieved successfully") response := models.ChainInfoResponse{ Height: height, ChainName: h.fabricClient.GetChannelName(), } writeJSONResponse(w, http.StatusOK, models.Response{ Success: true, Message: "Chain information retrieved successfully", Data: response, }) } // GetTransactionByID godoc // // @Summary Get transaction details by ID // @Description Retrieve detailed information about a specific transaction by its ID // @Tags transactions // @Accept json // @Produce json // @Param txid path string true "Transaction ID" // @Success 200 {object} models.Response{data=models.TransactionDetail} // @Failure 400 {object} models.ErrorResponse // @Failure 404 {object} models.ErrorResponse // @Failure 500 {object} models.ErrorResponse // @Router /transactions/{txid} [get] func (h *Handler) GetTransactionByID(w http.ResponseWriter, r *http.Request) { requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr) vars := mux.Vars(r) txID := vars["txid"] if txID == "" { requestLogger.Warn("Missing transaction ID in request") writeErrorResponse(w, http.StatusBadRequest, "Transaction ID is required") return } requestLogger.WithField("transaction_id", txID).Info("Getting transaction details") start := time.Now() txDetail, err := h.fabricClient.GetTransactionByID(txID) duration := time.Since(start) if err != nil { requestLogger.WithFields(logrus.Fields{ "error": err.Error(), "transaction_id": txID, "duration_ms": duration.Milliseconds(), }).Error("Failed to get transaction details") statusCode := http.StatusInternalServerError message := fmt.Sprintf("Failed to get transaction details: %s", fabric.HandleFabricError(err)) // Check if it's a "not found" error if strings.Contains(err.Error(), "not found") || strings.Contains(err.Error(), "does not exist") || strings.Contains(err.Error(), "no such transaction ID") { statusCode = http.StatusNotFound message = "Transaction not found" } writeErrorResponse(w, statusCode, message) return } requestLogger.WithFields(logrus.Fields{ "transaction_id": txID, "block_number": txDetail.BlockNumber, "duration_ms": duration.Milliseconds(), }).Info("Transaction details retrieved successfully") writeJSONResponse(w, http.StatusOK, models.Response{ Success: true, Message: "Transaction details retrieved successfully", Data: txDetail, }) }